diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index 86db9144cd3..068e21379c4 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -1,10 +1,9 @@ #!/usr/bin/env node -// Runs after install to restore bundled extension runtime deps. -// Installed builds can lazy-load bundled plugin code through root dist chunks, -// so runtime dependencies declared in dist/extensions/*/package.json must also -// resolve from the package root node_modules. Source checkouts resolve bundled -// plugin deps from the workspace root, so stale plugin-local node_modules must -// not linger under extensions/* and shadow the root graph. +// Runs after install to keep packaged dist safe and compatible. +// Bundled extension runtime dependencies are extension-owned. Do not install +// every bundled extension dependency during core package install unless the +// legacy eager-install escape hatch is explicitly enabled; `openclaw doctor +// --fix` owns the repair path for extensions that are actually used. import { spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { @@ -33,6 +32,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const DEFAULT_EXTENSIONS_DIR = join(__dirname, "..", "dist", "extensions"); const DEFAULT_PACKAGE_ROOT = join(__dirname, ".."); const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL"; +const EAGER_BUNDLED_PLUGIN_DEPS_ENV = "OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS"; const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json"; const LEGACY_UPDATE_COMPAT_SIDECARS = [ { @@ -465,6 +465,10 @@ export function createNestedNpmInstallEnv(env = process.env) { return nextEnv; } +function shouldEagerInstallBundledPluginDeps(env = process.env) { + return env?.[EAGER_BUNDLED_PLUGIN_DEPS_ENV]?.trim() === "1"; +} + export function applyBaileysEncryptedStreamFinishHotfix(params = {}) { const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT; const pathExists = params.existsSync ?? existsSync; @@ -714,6 +718,16 @@ export function runBundledPluginPostinstall(params = {}) { ) { return; } + if (!shouldEagerInstallBundledPluginDeps(env)) { + applyBundledPluginRuntimeHotfixes({ + packageRoot, + existsSync: pathExists, + readFileSync: params.readFileSync, + writeFileSync: params.writeFileSync, + log, + }); + return; + } const runtimeDeps = params.runtimeDeps ?? discoverBundledPluginRuntimeDeps({ extensionsDir, existsSync: pathExists }); diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 5f29f3cca16..3474b3654f7 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -24,6 +24,18 @@ vi.mock("../../agents/auth-profiles.js", () => ({ resolveAuthStorePathForDisplay: () => "/tmp/auth-profiles.json", })); +vi.mock("../../agents/auth-profiles/store.js", () => ({ + ensureAuthProfileStore: () => ({ + version: 1, + profiles: authProfilesStoreMock.profiles, + }), + findPersistedAuthProfileCredential: ({ profileId }: { profileId: string }) => + authProfilesStoreMock.profiles[profileId], + hasAnyAuthProfileStoreSource: () => Object.keys(authProfilesStoreMock.profiles).length > 0, + saveAuthProfileStore: vi.fn(), + updateAuthProfileStoreWithLock: vi.fn(), +})); + import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js"; import { clearRuntimeAuthProfileStoreSnapshots, diff --git a/test/scripts/postinstall-bundled-plugins.test.ts b/test/scripts/postinstall-bundled-plugins.test.ts index e89ed149adf..cf208d3e7cd 100644 --- a/test/scripts/postinstall-bundled-plugins.test.ts +++ b/test/scripts/postinstall-bundled-plugins.test.ts @@ -100,7 +100,7 @@ describe("bundled plugin postinstall", () => { }); }); - it("installs bundled plugin deps outside of source checkouts", async () => { + it("does not install bundled plugin deps outside of source checkouts by default", async () => { const extensionsDir = await createExtensionsDir(); const packageRoot = path.dirname(path.dirname(extensionsDir)); await writePluginPackage(extensionsDir, "acpx", { @@ -119,7 +119,7 @@ describe("bundled plugin postinstall", () => { log: { log: vi.fn(), warn: vi.fn() }, }); - expect(spawnSync).toHaveBeenCalled(); + expect(spawnSync).not.toHaveBeenCalled(); }); it("prunes source-checkout bundled plugin node_modules", async () => { @@ -431,6 +431,7 @@ describe("bundled plugin postinstall", () => { runBundledPluginPostinstall({ env: { + OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1", npm_config_global: "true", npm_config_location: "global", npm_config_prefix: "/opt/homebrew", @@ -494,7 +495,7 @@ describe("bundled plugin postinstall", () => { const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" })); runBundledPluginPostinstall({ - env: { HOME: "/tmp/home" }, + env: { HOME: "/tmp/home", OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1" }, extensionsDir, packageRoot, arch: "arm64", @@ -614,6 +615,7 @@ describe("bundled plugin postinstall", () => { runBundledPluginPostinstall({ env: { + OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1", npm_config_global: "true", npm_config_location: "global", npm_config_prefix: "/opt/homebrew", @@ -653,6 +655,7 @@ describe("bundled plugin postinstall", () => { runBundledPluginPostinstall({ env: { + OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1", HOME: "/tmp/home", }, extensionsDir, @@ -677,6 +680,7 @@ describe("bundled plugin postinstall", () => { runBundledPluginPostinstall({ env: { + OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1", npm_config_location: "global", npm_config_prefix: "/opt/homebrew", HOME: "/tmp/home",