fix: skip workspace plugin runtime deps

This commit is contained in:
Peter Steinberger
2026-04-21 07:30:39 +01:00
parent aacae4ce62
commit dc6ecd571a
4 changed files with 86 additions and 8 deletions

View File

@@ -30,7 +30,7 @@ Docs: https://docs.openclaw.ai
- OpenAI/Responses: resolve `/think` levels against each GPT model's supported reasoning efforts so `/think off` no longer becomes high reasoning or sends unsupported `reasoning.effort: "none"` payloads.
- Lobster/TaskFlow: allow managed approval resumes to use `approvalId` without a resume token, and persist that id in approval wait state. (#69559) Thanks @kirkluokun.
- Plugins/startup: install bundled runtime dependencies into each plugin's own runtime directory, reuse source-checkout repair caches after rebuilds, and log only packages that were actually installed so repeated Gateway starts stay quiet once deps are present.
- Plugins/startup: ignore pnpm's `npm_execpath` when repairing bundled plugin runtime dependencies so npm-only install flags are not passed to pnpm-launched gateways.
- Plugins/startup: ignore pnpm's `npm_execpath` when repairing bundled plugin runtime dependencies and skip workspace-only package specs so npm-only install flags or local workspace links do not break packaged plugin startup.
- Setup/TUI: relaunch the setup hatch TUI in a fresh process while preserving the configured gateway target and auth source, so onboarding recovers terminal state cleanly without exposing gateway secrets on command-line args. (#69524) Thanks @shakkernerd.
- Codex: avoid re-exposing the image-generation tool on native vision turns with inbound images, and keep bare image-model overrides on the configured image provider. (#65061) Thanks @zhulijin1991.
- Sessions/reset: clear auto-sourced model, provider, and auth-profile overrides on `/new` and `/reset` while preserving explicit user selections, so channel sessions stop staying pinned to runtime fallback choices. (#69419) Thanks @sk7n4k3d.

View File

@@ -43,8 +43,10 @@ describe("doctor bundled plugin runtime deps", () => {
writeJson(path.join(root, "dist", "extensions", "alpha", "package.json"), {
dependencies: {
"@openclaw/plugin-sdk": "workspace:*",
"dep-one": "1.0.0",
"@scope/dep-two": "2.0.0",
openclaw: "workspace:*",
},
optionalDependencies: {
"dep-opt": "3.0.0",

View File

@@ -262,6 +262,72 @@ describe("ensureBundledPluginRuntimeDeps", () => {
]);
});
it("skips workspace-only runtime deps before npm install", () => {
const packageRoot = makeTempDir();
const extensionsRoot = path.join(packageRoot, "dist", "extensions");
const pluginRoot = path.join(extensionsRoot, "qa-channel");
fs.mkdirSync(pluginRoot, { recursive: true });
fs.writeFileSync(
path.join(pluginRoot, "package.json"),
JSON.stringify({
dependencies: {
"@openclaw/plugin-sdk": "workspace:*",
"external-runtime": "^1.2.3",
openclaw: "workspace:*",
},
}),
);
const calls: BundledRuntimeDepsInstallParams[] = [];
const result = ensureBundledPluginRuntimeDeps({
env: {},
installDeps: (params) => {
calls.push(params);
},
pluginId: "qa-channel",
pluginRoot,
});
expect(result).toEqual({
installedSpecs: ["external-runtime@^1.2.3"],
retainSpecs: ["external-runtime@^1.2.3"],
});
expect(calls).toEqual([
{
installRoot: pluginRoot,
missingSpecs: ["external-runtime@^1.2.3"],
installSpecs: ["external-runtime@^1.2.3"],
},
]);
});
it("does not install when runtime deps are only workspace links", () => {
const packageRoot = makeTempDir();
const extensionsRoot = path.join(packageRoot, "dist", "extensions");
const pluginRoot = path.join(extensionsRoot, "qa-channel");
fs.mkdirSync(pluginRoot, { recursive: true });
fs.writeFileSync(
path.join(pluginRoot, "package.json"),
JSON.stringify({
dependencies: {
"@openclaw/plugin-sdk": "workspace:*",
openclaw: "workspace:*",
},
}),
);
const result = ensureBundledPluginRuntimeDeps({
env: {},
installDeps: () => {
throw new Error("workspace-only runtime deps should not install");
},
pluginId: "qa-channel",
pluginRoot,
});
expect(result).toEqual({ installedSpecs: [], retainSpecs: [] });
});
it("skips install when staged plugin-local runtime deps are present", () => {
const packageRoot = makeTempDir();
const extensionsRoot = path.join(packageRoot, "dist", "extensions");

View File

@@ -62,6 +62,17 @@ function collectRuntimeDeps(packageJson: JsonObject): Record<string, unknown> {
};
}
function normalizeInstallableRuntimeDepVersion(rawVersion: unknown): string | null {
if (typeof rawVersion !== "string") {
return null;
}
const version = rawVersion.trim();
if (version === "" || version.toLowerCase().startsWith("workspace:")) {
return null;
}
return version;
}
function isSourceCheckoutRoot(packageRoot: string): boolean {
return (
fs.existsSync(path.join(packageRoot, ".git")) &&
@@ -402,10 +413,10 @@ function collectBundledPluginRuntimeDeps(params: {
continue;
}
for (const [name, rawVersion] of Object.entries(collectRuntimeDeps(packageJson))) {
if (typeof rawVersion !== "string" || rawVersion.trim() === "") {
const version = normalizeInstallableRuntimeDepVersion(rawVersion);
if (!version) {
continue;
}
const version = rawVersion.trim();
const byVersion = versionMap.get(name) ?? new Map<string, Set<string>>();
const pluginIds = byVersion.get(version) ?? new Set<string>();
pluginIds.add(pluginId);
@@ -549,11 +560,10 @@ export function ensureBundledPluginRuntimeDeps(params: {
return { installedSpecs: [], retainSpecs: [] };
}
const deps = Object.entries(collectRuntimeDeps(packageJson))
.map(([name, rawVersion]) =>
typeof rawVersion === "string" && rawVersion.trim() !== ""
? { name, version: rawVersion.trim() }
: null,
)
.map(([name, rawVersion]) => {
const version = normalizeInstallableRuntimeDepVersion(rawVersion);
return version ? { name, version } : null;
})
.filter((entry): entry is { name: string; version: string } => Boolean(entry));
if (deps.length === 0) {
return { installedSpecs: [], retainSpecs: [] };