Skip to content

Commit 8cf724a

Browse files
authored
fix(plugins): simplify bundled runtime deps staging
* fix(plugins): simplify bundled runtime deps staging * refactor(plugins): declare bundled root runtime deps * fix(plugins): isolate pnpm runtime dependency installs * test(gateway): wait for deferred agent routing calls in server suite * test(ci): follow extracted update-channel assertions * fix(plugins): bypass pnpm age gate for bundled runtime deps * test: drop stale rebase leftovers * test: preserve mirrored root dependency drift guard * test: stage mirrored deps in facade fixtures * fix(plugin-sdk): expose provider setup metadata * test(plugin-sdk): satisfy spread lint in facade deps fixture * refactor(plugins): share bundled runtime deps install flow * fix(plugins): finish runtime deps rebase cleanup * fix(plugins): remove stale mirror import * refactor(plugins): centralize bundled runtime root preparation * fix(plugins): skip Windows pnpm cmd shims * refactor(plugins): let package managers own runtime deps staging * fix(plugins): validate staged runtime deps * fix(plugins): preserve lazy runtime deps fallback
1 parent 86f473d commit 8cf724a

25 files changed

Lines changed: 1774 additions & 1662 deletions

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ Docs: https://docs.openclaw.ai
107107
- Plugins: cache unchanged plugin manifest loads by file signature, reducing repeated JSON/JSON5 parsing and manifest normalization in bursty startup and runtime registry paths. Refs #73532 and #73647; carries forward #73678. Thanks @TheDutchRuler.
108108
- Plugins/runtime-deps: cache unchanged bundled runtime mirror dist-file materialization decisions and close file-lock handles on owner-write failures, reducing repeated startup chunk scans and avoiding FileHandle-GC recovery stalls. Refs #73532. Thanks @oadiazp and @bstanbury.
109109
- Plugins/runtime-deps: retry and defer transient cleanup failures for owned runtime staging directories so CLI startup no longer aborts after a successful bundled dependency swap. Refs #73903. Thanks @bobfreeman1989.
110-
- Plugins/runtime-deps: cache bundled runtime-deps JSON/package files and root chunk import scans by file signature, reducing repeated staged-runtime scanning during bundled channel startup. Refs #73647 and #73705. Thanks @mattmcintyre and @bmilne1981.
110+
- Plugins/runtime-deps: cache bundled runtime-deps JSON/package files by file signature, reducing repeated staged-runtime metadata reads during bundled channel startup. Refs #73647 and #73705. Thanks @mattmcintyre and @bmilne1981.
111+
- Plugins/runtime-deps: delegate bundled plugin dependency staging to complete npm/pnpm install plans with durable runtime state, removing retained-manifest and source-checkout cache reconciliation from Gateway startup. Refs #73532. Thanks @oadiazp, @bstanbury, and @jmfraga.
112+
- Plugins/runtime-deps: replace Gateway-start root chunk dependency inference with explicit mirrored-root dependency metadata, reducing staged runtime scans while preserving lazy per-plugin installs. Refs #73532. Thanks @oadiazp and @bstanbury.
113+
- Plugins/runtime-deps: run pnpm staged installs outside the repository workspace and disable pnpm release-age gates for exact bundled runtime dependency materialization, so bundled plugin dependency repair writes packages into the generated stage without blocking fresh packaged dependencies. Refs #73532. Thanks @oadiazp and @bstanbury.
111114
- CLI/TUI: keep `chat.history` off model-catalog discovery so initial Gateway-backed TUI history loads cannot block behind slow provider/plugin model scans on low-core hosts. Refs #73524. Thanks @harshcatsystems-collab.
112115
- Channels/WhatsApp: flag recently reconnected linked accounts in channel status even when the socket is currently healthy, so flapping WhatsApp Web sessions no longer look clean after a brief reconnect. Refs #73602. Thanks @Vksh07.
113116
- Channels/WhatsApp: log shared dispatcher delivery failures with reply kind, message id, chat id, and connection id, so typing-without-send reports can identify whether the WhatsApp send path rejected a generated reply. Refs #74269. Thanks @tomcosta-git.

docs/plugins/sdk-setup.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,8 @@ For npm-sourced installs, `openclaw plugins install` runs project-local `npm ins
520520
Bundled OpenClaw-owned plugins are the only startup repair exception: when a packaged install sees one enabled by plugin config, legacy channel config, or its bundled default-enabled manifest, startup installs that plugin's missing runtime dependencies before import. Third-party plugins should not rely on startup installs; keep using the explicit plugin installer.
521521
</Note>
522522

523+
Bundled package-level runtime deps are explicit metadata, not inferred from built JavaScript at gateway startup. If a shared OpenClaw root dependency must be available inside the external bundled-plugin runtime mirror, declare it in `openclaw.bundle.mirroredRootRuntimeDependencies` in the root package manifest.
524+
523525
## Related
524526

525527
- [Building plugins](/plugins/building-plugins) — step-by-step getting started guide

package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,5 +1728,23 @@
17281728
"@whiskeysockets/[email protected]": "patches/@[email protected]",
17291729
"@agentclientprotocol/[email protected]": "patches/@[email protected]"
17301730
}
1731+
},
1732+
"openclaw": {
1733+
"bundle": {
1734+
"mirroredRootRuntimeDependencies": [
1735+
"@agentclientprotocol/sdk",
1736+
"@lydell/node-pty",
1737+
"croner",
1738+
"dotenv",
1739+
"jiti",
1740+
"json5",
1741+
"jszip",
1742+
"markdown-it",
1743+
"semver",
1744+
"tar",
1745+
"tslog",
1746+
"web-push"
1747+
]
1748+
}
17311749
}
17321750
}

scripts/lib/bundled-plugin-root-runtime-mirrors.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,28 @@ export function collectBundledPluginRootRuntimeMirrorErrors(params) {
213213

214214
return errors.toSorted((left, right) => left.localeCompare(right));
215215
}
216+
217+
export function collectDeclaredRootRuntimeDependencyMetadataErrors(rootPackageJson) {
218+
const declaredRootRuntimeDeps = collectRuntimeDependencySpecs(rootPackageJson);
219+
const declaredMirrorDeps =
220+
rootPackageJson?.openclaw?.bundle?.mirroredRootRuntimeDependencies ?? [];
221+
if (!Array.isArray(declaredMirrorDeps)) {
222+
return ["package.json openclaw.bundle.mirroredRootRuntimeDependencies must be an array."];
223+
}
224+
225+
const errors = [];
226+
for (const dependencyName of declaredMirrorDeps) {
227+
if (typeof dependencyName !== "string" || dependencyName.trim().length === 0) {
228+
errors.push(
229+
"package.json openclaw.bundle.mirroredRootRuntimeDependencies entries must be non-empty strings.",
230+
);
231+
continue;
232+
}
233+
if (!declaredRootRuntimeDeps.has(dependencyName)) {
234+
errors.push(
235+
`package.json openclaw.bundle.mirroredRootRuntimeDependencies declares '${dependencyName}' but package.json dependencies/optionalDependencies do not include it.`,
236+
);
237+
}
238+
}
239+
return errors.toSorted((left, right) => left.localeCompare(right));
240+
}

scripts/release-check.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
collectBuiltBundledPluginStagedRuntimeDependencyErrors,
3636
collectBundledPluginRootRuntimeMirrorErrors,
3737
collectBundledPluginRuntimeDependencySpecs,
38+
collectDeclaredRootRuntimeDependencyMetadataErrors,
3839
collectRootDistBundledRuntimeMirrors,
3940
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
4041
import { collectPackUnpackedSizeErrors as collectNpmPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs";
@@ -52,6 +53,7 @@ export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-m
5253
export {
5354
collectBuiltBundledPluginStagedRuntimeDependencyErrors,
5455
collectBundledPluginRootRuntimeMirrorErrors,
56+
collectDeclaredRootRuntimeDependencyMetadataErrors,
5557
collectRootDistBundledRuntimeMirrors,
5658
packageNameFromSpecifier,
5759
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
@@ -162,10 +164,16 @@ function checkBundledExtensionMetadata() {
162164
requiredRootMirrors,
163165
rootPackageJson: rootPackage,
164166
});
167+
const rootMirrorMetadataErrors = collectDeclaredRootRuntimeDependencyMetadataErrors(rootPackage);
165168
const builtArtifactErrors = collectBuiltBundledPluginStagedRuntimeDependencyErrors({
166169
bundledPluginsDir: resolve("dist/extensions"),
167170
});
168-
const errors = [...manifestErrors, ...rootMirrorErrors, ...builtArtifactErrors];
171+
const errors = [
172+
...manifestErrors,
173+
...rootMirrorErrors,
174+
...rootMirrorMetadataErrors,
175+
...builtArtifactErrors,
176+
];
169177
if (errors.length > 0) {
170178
console.error("release-check: bundled extension manifest validation failed:");
171179
for (const error of errors) {

scripts/test-built-bundled-runtime-deps.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
collectBuiltBundledPluginStagedRuntimeDependencyErrors,
99
collectBundledPluginRootRuntimeMirrorErrors,
1010
collectBundledPluginRuntimeDependencySpecs,
11+
collectDeclaredRootRuntimeDependencyMetadataErrors,
1112
collectRootDistBundledRuntimeMirrors,
1213
} from "./lib/bundled-plugin-root-runtime-mirrors.mjs";
1314
import { parsePackageRootArg } from "./lib/package-root-args.mjs";
@@ -36,6 +37,7 @@ const errors = [
3637
requiredRootMirrors,
3738
rootPackageJson,
3839
}),
40+
...collectDeclaredRootRuntimeDependencyMetadataErrors(rootPackageJson),
3941
...collectBuiltBundledPluginStagedRuntimeDependencyErrors({
4042
bundledPluginsDir: builtPluginsDir,
4143
}),

src/channels/plugins/bundled.shape-guard.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import os from "node:os";
33
import path from "node:path";
4+
import { pathToFileURL } from "node:url";
45
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
56
import { afterEach, describe, expect, it, vi } from "vitest";
67
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
@@ -651,6 +652,102 @@ describe("bundled channel entry shape guards", () => {
651652
}
652653
});
653654

655+
it("does not load bundled runtime entries through external staged runtime deps during discovery", async () => {
656+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-runtime-deps-"));
657+
const stageRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-stage-"));
658+
const previousBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
659+
const previousPluginStageDir = process.env.OPENCLAW_PLUGIN_STAGE_DIR;
660+
const pluginDir = path.join(root, "dist", "extensions", "alpha");
661+
const testGlobal = globalThis as typeof globalThis & {
662+
__bundledRuntimeDepMarker?: string;
663+
};
664+
fs.mkdirSync(pluginDir, { recursive: true });
665+
fs.writeFileSync(
666+
path.join(root, "package.json"),
667+
JSON.stringify({ name: "openclaw", version: "2026.4.21" }),
668+
"utf8",
669+
);
670+
fs.writeFileSync(
671+
path.join(pluginDir, "package.json"),
672+
JSON.stringify({
673+
name: "@openclaw/alpha",
674+
version: "2026.4.21",
675+
type: "module",
676+
dependencies: {
677+
"alpha-runtime-dep": "1.0.0",
678+
},
679+
}),
680+
"utf8",
681+
);
682+
fs.writeFileSync(
683+
path.join(pluginDir, "plugin.js"),
684+
[
685+
"import { marker } from 'alpha-runtime-dep';",
686+
"globalThis.__bundledRuntimeDepMarker = marker;",
687+
"export default { id: 'alpha', meta: { label: marker }, config: {} };",
688+
"",
689+
].join("\n"),
690+
"utf8",
691+
);
692+
fs.writeFileSync(
693+
path.join(pluginDir, "index.js"),
694+
[
695+
`import { defineBundledChannelEntry } from ${JSON.stringify(pathToFileURL(path.resolve("src/plugin-sdk/channel-entry-contract.ts")).href)};`,
696+
"export default defineBundledChannelEntry({",
697+
" id: 'alpha',",
698+
" name: 'Alpha',",
699+
" description: 'Alpha',",
700+
" importMetaUrl: import.meta.url,",
701+
" plugin: { specifier: './plugin.js' },",
702+
"});",
703+
"",
704+
].join("\n"),
705+
"utf8",
706+
);
707+
708+
process.env.OPENCLAW_PLUGIN_STAGE_DIR = stageRoot;
709+
const { resolveBundledRuntimeDependencyInstallRoot } =
710+
await import("../../plugins/bundled-runtime-deps.js");
711+
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginDir);
712+
const depRoot = path.join(installRoot, "node_modules", "alpha-runtime-dep");
713+
fs.mkdirSync(depRoot, { recursive: true });
714+
fs.writeFileSync(
715+
path.join(depRoot, "package.json"),
716+
JSON.stringify({
717+
name: "alpha-runtime-dep",
718+
version: "1.0.0",
719+
type: "module",
720+
main: "index.js",
721+
}),
722+
"utf8",
723+
);
724+
fs.writeFileSync(path.join(depRoot, "index.js"), "export const marker = 'staged-alpha';\n");
725+
726+
mockAlphaDistExtensionRuntime();
727+
728+
try {
729+
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(root, "dist", "extensions");
730+
731+
const bundled = await importFreshModule<typeof import("./bundled.js")>(
732+
import.meta.url,
733+
"./bundled.js?scope=bundled-runtime-deps",
734+
);
735+
736+
expect(bundled.getBundledChannelPlugin("alpha")).toBeUndefined();
737+
expect(testGlobal.__bundledRuntimeDepMarker).toBeUndefined();
738+
} finally {
739+
restoreBundledPluginsDir(previousBundledPluginsDir);
740+
if (previousPluginStageDir === undefined) {
741+
delete process.env.OPENCLAW_PLUGIN_STAGE_DIR;
742+
} else {
743+
process.env.OPENCLAW_PLUGIN_STAGE_DIR = previousPluginStageDir;
744+
}
745+
fs.rmSync(root, { recursive: true, force: true });
746+
fs.rmSync(stageRoot, { recursive: true, force: true });
747+
delete testGlobal.__bundledRuntimeDepMarker;
748+
}
749+
});
750+
654751
it("swallows and caches bundled plugin and setup load failures", async () => {
655752
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-load-failure-"));
656753
const previousBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;

src/channels/plugins/bundled.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ type BundledChannelEntryRuntimeContract = {
3636
accountInspect?: boolean;
3737
};
3838
register: (api: unknown) => void;
39-
loadChannelPlugin: () => ChannelPlugin;
40-
loadChannelSecrets?: () => ChannelPlugin["secrets"] | undefined;
41-
loadChannelAccountInspector?: () => NonNullable<ChannelPlugin["config"]["inspectAccount"]>;
39+
loadChannelPlugin: (options?: BundledEntryModuleLoadOptions) => ChannelPlugin;
40+
loadChannelSecrets?: (
41+
options?: BundledEntryModuleLoadOptions,
42+
) => ChannelPlugin["secrets"] | undefined;
43+
loadChannelAccountInspector?: (
44+
options?: BundledEntryModuleLoadOptions,
45+
) => NonNullable<ChannelPlugin["config"]["inspectAccount"]>;
4246
setChannelRuntime?: (runtime: PluginRuntime) => void;
4347
};
4448

@@ -239,7 +243,7 @@ function loadGeneratedBundledChannelEntry(params: {
239243
rootScope: params.rootScope,
240244
metadata: params.metadata,
241245
entry: params.metadata.source,
242-
installRuntimeDeps: true,
246+
installRuntimeDeps: false,
243247
}),
244248
);
245249
if (!entry) {
@@ -586,7 +590,7 @@ function getBundledChannelSecretsForRoot(
586590
}
587591
try {
588592
const secrets =
589-
entry.loadChannelSecrets?.() ??
593+
entry.loadChannelSecrets?.({ installRuntimeDeps: false }) ??
590594
getBundledChannelPluginForRoot(id, rootScope, loadContext)?.secrets;
591595
loadContext.lazySecretsById.set(id, secrets ?? null);
592596
return secrets;
@@ -612,7 +616,7 @@ function getBundledChannelAccountInspectorForRoot(
612616
return undefined;
613617
}
614618
try {
615-
const inspector = entry.loadChannelAccountInspector();
619+
const inspector = entry.loadChannelAccountInspector({ installRuntimeDeps: false });
616620
loadContext.lazyAccountInspectorsById.set(id, inspector);
617621
return inspector;
618622
} catch (error) {

0 commit comments

Comments
 (0)