diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf1ffe1892..84bc89dd824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/doctor-bundled-plugin-runtime-deps.test.ts b/src/commands/doctor-bundled-plugin-runtime-deps.test.ts index 72ebf7cd974..ce295453fe5 100644 --- a/src/commands/doctor-bundled-plugin-runtime-deps.test.ts +++ b/src/commands/doctor-bundled-plugin-runtime-deps.test.ts @@ -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", diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index f5bc40bc2b9..5bf8cd819ac 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -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"); diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index a1b5679e2cc..e7043fc4e6e 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -62,6 +62,17 @@ function collectRuntimeDeps(packageJson: JsonObject): Record { }; } +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>(); const pluginIds = byVersion.get(version) ?? new Set(); 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: [] };