mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix: harden openclaw peer dependency installs (#70462)
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Auto-reply/system events: route async exec-event completion replies through the persisted session delivery context, so long-running command results return to the originating channel instead of being dropped when live origin metadata is missing. (#70258) Thanks @wzfukui.
|
||||
- QA channel/security: reject non-HTTP(S) inbound attachment URLs before media fetch, and log rejected schemes so suspicious or misconfigured payloads are visible during debugging. (#70708) Thanks @vincentkoc.
|
||||
- Plugins/install: link the host OpenClaw package into external plugins that declare `openclaw` as a peer dependency, so peer-only plugin SDK imports resolve after install without bundling a duplicate host package. (#70462) Thanks @anishesg.
|
||||
- Teams/security: require shared Bot Framework audience tokens to name the configured Teams app via verified `appid` or `azp`, blocking cross-bot token replay on the global audience. (#70724) Thanks @vincentkoc.
|
||||
- Plugins/startup: resolve bundled plugin Jiti loads relative to the target plugin module instead of the central loader, so Bun global installs no longer hang while discovering bundled image providers. (#70073) Thanks @yidianyiko.
|
||||
- Anthropic/CLI security: stop Claude CLI backend defaults from forcing `bypassPermissions`, and strip malformed permission-mode overrides instead of silently falling back to a bypass. (#70723) Thanks @vincentkoc.
|
||||
|
||||
@@ -3,13 +3,13 @@ import path from "node:path";
|
||||
import * as tar from "tar";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { safePathSegmentHashed } from "../infra/install-safe-path.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { expectSingleNpmInstallIgnoreScriptsCall } from "../test-utils/exec-assertions.js";
|
||||
import { expectInstallUsesIgnoreScripts } from "../test-utils/npm-spec-install-test-helpers.js";
|
||||
import { initializeGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
|
||||
import { createMockPluginRegistry } from "./hooks.test-helpers.js";
|
||||
import * as installSecurityScan from "./install-security-scan.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import {
|
||||
installPluginFromArchive,
|
||||
installPluginFromDir,
|
||||
@@ -2380,6 +2380,7 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
it("creates a node_modules/openclaw symlink when peerDependencies declares openclaw", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const fakeHostRoot = suiteTempRootTracker.makeTempDir();
|
||||
const run = vi.mocked(runCommandWithTimeout);
|
||||
resolveRootMock.mockReturnValue(fakeHostRoot);
|
||||
|
||||
writePluginWithPeerDeps(pluginDir, { openclaw: "*" });
|
||||
@@ -2395,6 +2396,7 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
const stat = fs.lstatSync(symlinkPath);
|
||||
expect(stat.isSymbolicLink()).toBe(true);
|
||||
expect(fs.realpathSync(symlinkPath)).toBe(fs.realpathSync(fakeHostRoot));
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not create a symlink when peerDependencies is empty", async () => {
|
||||
@@ -2415,7 +2417,7 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
expect(fs.existsSync(symlinkPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("is idempotent — re-installing replaces an existing symlink without error", async () => {
|
||||
it("is idempotent - re-installing replaces an existing symlink without error", async () => {
|
||||
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
|
||||
const fakeHostRoot = suiteTempRootTracker.makeTempDir();
|
||||
resolveRootMock.mockReturnValue(fakeHostRoot);
|
||||
@@ -2426,7 +2428,7 @@ describe("linkOpenClawPeerDependencies (via installPluginFromDir)", () => {
|
||||
const { result: first } = await installFromDirWithWarnings({ pluginDir, extensionsDir });
|
||||
expect(first.ok).toBe(true);
|
||||
|
||||
// Second install (update mode) — should replace symlink, not throw
|
||||
// Second install (update mode) should replace symlink, not throw.
|
||||
const { result: second, warnings } = await installFromDirWithWarnings({
|
||||
pluginDir,
|
||||
extensionsDir,
|
||||
|
||||
@@ -611,10 +611,8 @@ async function detectNativePackageInstallSource(packageDir: string): Promise<boo
|
||||
}
|
||||
|
||||
/**
|
||||
* After npm install completes, symlink any peerDependencies that name the host
|
||||
* openclaw package into the plugin's node_modules/ directory. npm never
|
||||
* materialises peerDependencies automatically, so plugins that moved openclaw
|
||||
* from dependencies → peerDependencies would fail at runtime without this.
|
||||
* After the staged plugin tree has been scanned, symlink the host openclaw
|
||||
* package for plugins that declare it as a peer dependency.
|
||||
*/
|
||||
async function linkOpenClawPeerDependencies(params: {
|
||||
installedDir: string;
|
||||
@@ -643,31 +641,15 @@ async function linkOpenClawPeerDependencies(params: {
|
||||
|
||||
for (const peerName of peers) {
|
||||
const linkPath = path.join(nodeModulesDir, peerName);
|
||||
// Resolve the actual source for scoped packages (e.g. @scope/name).
|
||||
const linkTarget = path.join(hostRoot, "node_modules", peerName);
|
||||
// Check whether the package exists at the expected location inside the
|
||||
// host root's node_modules. If it does not (e.g. openclaw IS the root
|
||||
// package), fall back to the host root itself.
|
||||
let resolvedTarget: string;
|
||||
try {
|
||||
await fs.access(path.join(linkTarget, "package.json"));
|
||||
resolvedTarget = linkTarget;
|
||||
} catch {
|
||||
resolvedTarget = hostRoot;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove any existing entry (broken link or stale directory) before
|
||||
// creating the new symlink so re-installs are idempotent.
|
||||
await fs.rm(linkPath, { recursive: true, force: true });
|
||||
await fs.symlink(resolvedTarget, linkPath, "junction");
|
||||
params.logger.info?.(
|
||||
`Linked peerDependency "${peerName}" → ${resolvedTarget}`,
|
||||
);
|
||||
await fs.symlink(hostRoot, linkPath, "junction");
|
||||
params.logger.info?.(`Linked peerDependency "${peerName}" -> ${hostRoot}`);
|
||||
} catch (err) {
|
||||
params.logger.warn?.(
|
||||
`Failed to symlink peerDependency "${peerName}": ${String(err)}`,
|
||||
);
|
||||
params.logger.warn?.(`Failed to symlink peerDependency "${peerName}": ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -820,7 +802,7 @@ async function installPluginFromPackageDir(
|
||||
mode: targetResult.target.effectiveMode,
|
||||
dryRun,
|
||||
copyErrorPrefix: "failed to copy plugin",
|
||||
hasDeps: Object.keys(deps).length > 0 || Object.keys(peerDeps).length > 0,
|
||||
hasDeps: Object.keys(deps).length > 0,
|
||||
depsLogMessage: "Installing plugin dependencies…",
|
||||
nameEncoder: encodePluginInstallDirName,
|
||||
afterCopy: async (installedDir) => {
|
||||
|
||||
Reference in New Issue
Block a user