fix(plugins): apply npm overrides to managed roots (#78386)

This commit is contained in:
Vincent Koc
2026-05-06 02:47:25 -07:00
committed by GitHub
parent b895c6d939
commit 5d557171b3
6 changed files with 264 additions and 1 deletions

View File

@@ -119,6 +119,7 @@ Docs: https://docs.openclaw.ai
- Codex/app-server: forward the OpenClaw workspace bootstrap block through Codex `developerInstructions` instead of `config.instructions`, so persona/style guidance reaches the behavior-shaping app-server lane. Fixes #77363. Thanks @lonexreb.
- CLI/infer: pass minimal instructions to local `openai-codex/*` model probes and surface provider error details when `infer model run` returns no text. Fixes #76464. Thanks @lilesjtu.
- Dependencies: override transitive `ip-address` to `10.2.0` so the runtime lockfile no longer includes the vulnerable `10.1.0` build flagged by Dependabot alert 109. Thanks @vincentkoc.
- Plugins/install: apply OpenClaw's npm security overrides inside managed external plugin npm roots so hoisted plugin dependencies inherit the host package hardening. Thanks @vincentkoc.
- Feishu: hydrate missing native topic starter thread IDs before session routing so first turns and follow-ups stay in the same topic session. Fixes #78262. Thanks @joeyzenghuan.
- LINE: reject `dmPolicy: "open"` configs without wildcard `allowFrom` so webhook DMs fail validation instead of being acknowledged and silently blocked before inbound processing. Fixes #78316.
- Telegram/Codex: keep message-tool-only progress drafts visible and render native Codex tool progress once per tool instead of duplicating item/tool draft lines. Fixes #75641. (#77949) Thanks @keshavbotagent.

View File

@@ -131,7 +131,7 @@ is available, then fall back to `latest`.
<Accordion title="Hook packs and npm specs">
`plugins install` is also the install surface for hook packs that expose `openclaw.hooks` in `package.json`. Use `openclaw hooks` for filtered hook visibility and per-hook enablement, not package installation.
Npm specs are **registry-only** (package name + optional **exact version** or **dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency installs run project-local with `--ignore-scripts` for safety, even when your shell has global npm install settings.
Npm specs are **registry-only** (package name + optional **exact version** or **dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency installs run project-local with `--ignore-scripts` for safety, even when your shell has global npm install settings. Managed plugin npm roots inherit OpenClaw's package-level npm `overrides`, so host security pins apply to hoisted plugin dependencies too.
Use `npm:<package>` when you want to make npm resolution explicit. Bare package specs also install directly from npm during the launch cutover.

View File

@@ -162,6 +162,9 @@ managed npm root. After npm finishes, OpenClaw verifies the installed
`package-lock.json` entry still matches the resolved version and integrity. If
npm writes different package metadata, the install fails and the managed package
is rolled back instead of accepting a different plugin artifact.
Managed npm roots also inherit OpenClaw's package-level npm `overrides`, so
security pins that protect the packaged host also apply to hoisted external
plugin dependencies.
Source checkouts are pnpm workspaces. If you clone OpenClaw to hack on bundled
plugins, run `pnpm install`; OpenClaw then loads bundled plugins from

View File

@@ -1,11 +1,13 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
repairManagedNpmRootOpenClawPeer,
removeManagedNpmRootDependency,
readManagedNpmRootInstalledDependency,
readOpenClawManagedNpmRootOverrides,
resolveManagedNpmRootDependencySpec,
upsertManagedNpmRootDependency,
} from "./npm-managed-root.js";
@@ -71,6 +73,127 @@ describe("managed npm root", () => {
});
});
it("syncs OpenClaw-owned overrides without dropping unrelated local overrides", async () => {
const npmRoot = await makeTempRoot();
await fs.writeFile(
path.join(npmRoot, "package.json"),
`${JSON.stringify(
{
private: true,
dependencies: {
"@openclaw/discord": "2026.5.2",
},
overrides: {
axios: "1.13.6",
"left-pad": "1.3.0",
qs: "6.14.0",
},
openclaw: {
managedOverrides: ["axios", "qs"],
},
},
null,
2,
)}\n`,
);
await upsertManagedNpmRootDependency({
npmRoot,
packageName: "@openclaw/feishu",
dependencySpec: "2026.5.4",
managedOverrides: {
axios: "1.16.0",
"node-domexception": "npm:@nolyfill/domexception@1.0.28",
},
});
await expect(
fs.readFile(path.join(npmRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)),
).resolves.toEqual({
private: true,
dependencies: {
"@openclaw/discord": "2026.5.2",
"@openclaw/feishu": "2026.5.4",
},
overrides: {
"left-pad": "1.3.0",
axios: "1.16.0",
"node-domexception": "npm:@nolyfill/domexception@1.0.28",
},
openclaw: {
managedOverrides: ["axios", "node-domexception"],
},
});
});
it("reads package-level npm overrides for managed plugin installs", async () => {
await expect(readOpenClawManagedNpmRootOverrides()).resolves.toMatchObject({
axios: "1.16.0",
});
});
it("resolves package-level npm overrides from packaged dist chunks", async () => {
const packageRoot = await makeTempRoot();
await fs.mkdir(path.join(packageRoot, "dist"), { recursive: true });
await fs.writeFile(
path.join(packageRoot, "package.json"),
`${JSON.stringify(
{
name: "openclaw",
overrides: {
axios: "1.16.0",
},
},
null,
2,
)}\n`,
);
await expect(
readOpenClawManagedNpmRootOverrides({
moduleUrl: pathToFileURL(path.join(packageRoot, "dist", "install-AbCdEf.js")).toString(),
cwd: path.join(packageRoot, "dist"),
}),
).resolves.toEqual({
axios: "1.16.0",
});
});
it("resolves npm override dependency references from the host package manifest", async () => {
const packageRoot = await makeTempRoot();
await fs.writeFile(
path.join(packageRoot, "package.json"),
`${JSON.stringify(
{
name: "openclaw",
dependencies: {
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
},
optionalDependencies: {
"optional-runtime": "2.0.0",
},
overrides: {
"@aws-sdk/client-bedrock-runtime": "$@aws-sdk/client-bedrock-runtime",
nested: {
"optional-runtime": "$optional-runtime",
},
axios: "1.16.0",
},
},
null,
2,
)}\n`,
);
await expect(readOpenClawManagedNpmRootOverrides({ packageRoot })).resolves.toEqual({
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
nested: {
"optional-runtime": "2.0.0",
},
axios: "1.16.0",
});
});
it("does not overwrite a present malformed package manifest", async () => {
const npmRoot = await makeTempRoot();
const manifestPath = path.join(npmRoot, "package.json");

View File

@@ -4,11 +4,26 @@ import { runCommandWithTimeout } from "../process/exec.js";
import type { NpmSpecResolution } from "./install-source-utils.js";
import { readJson, readJsonIfExists, writeJson } from "./json-files.js";
import type { ParsedRegistryNpmSpec } from "./npm-registry-spec.js";
import { resolveOpenClawPackageRootSync } from "./openclaw-root.js";
import { createSafeNpmInstallEnv } from "./safe-package-install.js";
type ManagedNpmRootManifest = {
private?: boolean;
dependencies?: Record<string, string>;
overrides?: Record<string, unknown>;
[key: string]: unknown;
};
type HostPackageManifest = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
overrides?: Record<string, unknown>;
peerDependencies?: Record<string, string>;
};
type ManagedNpmRootOpenClawMetadata = {
managedOverrides?: string[];
[key: string]: unknown;
};
@@ -51,11 +66,108 @@ function readDependencyRecord(value: unknown): Record<string, string> {
return dependencies;
}
function readOverrideRecord(value: unknown): Record<string, unknown> {
if (!isRecord(value)) {
return {};
}
const overrides: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(value)) {
if (key.trim()) {
overrides[key] = raw;
}
}
return overrides;
}
function readManagedOverrideKeys(value: unknown): string[] {
if (!isRecord(value) || !Array.isArray(value.managedOverrides)) {
return [];
}
return value.managedOverrides.filter((key): key is string => typeof key === "string");
}
function buildManagedOpenClawMetadata(params: {
current: unknown;
managedOverrideKeys: string[];
}): ManagedNpmRootOpenClawMetadata | undefined {
const metadata: ManagedNpmRootOpenClawMetadata = isRecord(params.current)
? { ...params.current }
: {};
if (params.managedOverrideKeys.length > 0) {
metadata.managedOverrides = params.managedOverrideKeys;
} else {
delete metadata.managedOverrides;
}
return Object.keys(metadata).length > 0 ? metadata : undefined;
}
async function readManagedNpmRootManifest(filePath: string): Promise<ManagedNpmRootManifest> {
const parsed = await readJsonIfExists<unknown>(filePath);
return isRecord(parsed) ? { ...parsed } : {};
}
function readHostDependencySpec(
manifest: HostPackageManifest,
packageName: string,
): string | undefined {
return (
manifest.dependencies?.[packageName] ??
manifest.optionalDependencies?.[packageName] ??
manifest.peerDependencies?.[packageName] ??
manifest.devDependencies?.[packageName]
);
}
function resolveHostOverrideReferences(value: unknown, manifest: HostPackageManifest): unknown {
if (typeof value === "string" && value.startsWith("$")) {
return readHostDependencySpec(manifest, value.slice(1)) ?? value;
}
if (!isRecord(value)) {
return value;
}
const resolved: Record<string, unknown> = {};
for (const [key, nested] of Object.entries(value)) {
resolved[key] = resolveHostOverrideReferences(nested, manifest);
}
return resolved;
}
export async function readOpenClawManagedNpmRootOverrides(params?: {
argv1?: string;
cwd?: string;
moduleUrl?: string;
packageRoot?: string | null;
}): Promise<Record<string, unknown>> {
const packageRoot =
params?.packageRoot ??
resolveOpenClawPackageRootSync({
argv1: params?.argv1 ?? process.argv[1],
moduleUrl: params?.moduleUrl ?? import.meta.url,
cwd: params?.cwd ?? process.cwd(),
});
if (!packageRoot) {
return {};
}
try {
const manifest = JSON.parse(
await fs.readFile(path.join(packageRoot, "package.json"), "utf8"),
) as unknown;
if (!isRecord(manifest)) {
return {};
}
const hostManifest = manifest as HostPackageManifest;
const overrides = readOverrideRecord(hostManifest.overrides);
return Object.fromEntries(
Object.entries(overrides).map(([key, value]) => [
key,
resolveHostOverrideReferences(value, hostManifest),
]),
);
} catch {
return {};
}
}
export function resolveManagedNpmRootDependencySpec(params: {
parsedSpec: ParsedRegistryNpmSpec;
resolution: NpmSpecResolution;
@@ -67,11 +179,23 @@ export async function upsertManagedNpmRootDependency(params: {
npmRoot: string;
packageName: string;
dependencySpec: string;
managedOverrides?: Record<string, unknown>;
}): Promise<void> {
await fs.mkdir(params.npmRoot, { recursive: true });
const manifestPath = path.join(params.npmRoot, "package.json");
const manifest = await readManagedNpmRootManifest(manifestPath);
const dependencies = readDependencyRecord(manifest.dependencies);
const managedOverrides = readOverrideRecord(params.managedOverrides);
const managedOverrideKeys = Object.keys(managedOverrides).toSorted();
const overrides = readOverrideRecord(manifest.overrides);
for (const key of readManagedOverrideKeys(manifest.openclaw)) {
delete overrides[key];
}
Object.assign(overrides, managedOverrides);
const openclawMetadata = buildManagedOpenClawMetadata({
current: manifest.openclaw,
managedOverrideKeys,
});
const next: ManagedNpmRootManifest = {
...manifest,
private: true,
@@ -80,6 +204,16 @@ export async function upsertManagedNpmRootDependency(params: {
[params.packageName]: params.dependencySpec,
},
};
if (Object.keys(overrides).length > 0) {
next.overrides = overrides;
} else {
delete next.overrides;
}
if (openclawMetadata) {
next.openclaw = openclawMetadata;
} else {
delete next.openclaw;
}
await writeJson(manifestPath, next, { trailingNewline: true });
}

View File

@@ -11,6 +11,7 @@ import {
import { resolveNpmIntegrityDriftWithDefaultMessage } from "../infra/npm-integrity.js";
import {
readManagedNpmRootInstalledDependency,
readOpenClawManagedNpmRootOverrides,
repairManagedNpmRootOpenClawPeer,
removeManagedNpmRootDependency,
resolveManagedNpmRootDependencySpec,
@@ -485,6 +486,7 @@ async function installPluginFromManagedNpmRoot(
npmRoot,
packageName: params.packageName,
dependencySpec: params.dependencySpec,
managedOverrides: await readOpenClawManagedNpmRootOverrides(),
});
const install = await runCommandWithTimeout(
[