mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix(plugins): apply npm overrides to managed roots (#78386)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user