mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 15:18:58 +00:00
fix(plugins): skip managed npm peer resolution
This commit is contained in:
@@ -109,6 +109,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- 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: skip npm peer resolution in managed plugin roots so installing peer-based plugins such as Opik cannot pull a stale registry `openclaw` copy beside Codex/Discord/WhatsApp and trigger `ERESOLVE`. 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.
|
||||
|
||||
@@ -43,7 +43,7 @@ OpenClaw uses stable per-source roots:
|
||||
npm installs run in the npm root with:
|
||||
|
||||
```bash
|
||||
npm install --prefix ~/.openclaw/npm <spec> --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
npm install --prefix ~/.openclaw/npm <spec> --omit=dev --omit=peer --legacy-peer-deps --ignore-scripts --no-audit --no-fund
|
||||
```
|
||||
|
||||
npm may hoist transitive dependencies to `~/.openclaw/npm/node_modules` beside
|
||||
@@ -54,10 +54,10 @@ runtime dependencies stay inside the managed cleanup boundary.
|
||||
Plugins that import `openclaw/plugin-sdk/*` declare `openclaw` as a peer
|
||||
dependency. OpenClaw does not let npm install a separate registry copy of the
|
||||
host package into the managed root, because stale host packages can affect npm
|
||||
peer resolution during later plugin installs. Instead, after npm finishes
|
||||
mutating the shared root during install, update, or uninstall, OpenClaw reasserts
|
||||
peer resolution during later plugin installs. Managed npm installs skip npm peer
|
||||
resolution/materialization for the shared root and OpenClaw reasserts
|
||||
plugin-local `node_modules/openclaw` links for installed packages that declare
|
||||
the host peer.
|
||||
the host peer after install, update, or uninstall.
|
||||
|
||||
git installs clone or refresh the repository, then run:
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ describe("safe npm install helpers", () => {
|
||||
expect(
|
||||
createSafeNpmInstallArgs({
|
||||
omitDev: true,
|
||||
omitPeer: true,
|
||||
legacyPeerDeps: true,
|
||||
ignoreWorkspaces: true,
|
||||
loglevel: "error",
|
||||
noAudit: true,
|
||||
@@ -14,6 +16,8 @@ describe("safe npm install helpers", () => {
|
||||
).toEqual([
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--omit=peer",
|
||||
"--legacy-peer-deps",
|
||||
"--loglevel=error",
|
||||
"--ignore-scripts",
|
||||
"--workspaces=false",
|
||||
|
||||
@@ -10,10 +10,12 @@ type SafeNpmInstallEnvOptions = NpmProjectInstallEnvOptions & {
|
||||
|
||||
type SafeNpmInstallArgsOptions = {
|
||||
ignoreWorkspaces?: boolean;
|
||||
legacyPeerDeps?: boolean;
|
||||
loglevel?: "error" | "silent";
|
||||
noAudit?: boolean;
|
||||
noFund?: boolean;
|
||||
omitDev?: boolean;
|
||||
omitPeer?: boolean;
|
||||
};
|
||||
|
||||
export function createSafeNpmInstallEnv(
|
||||
@@ -47,6 +49,8 @@ export function createSafeNpmInstallArgs(options: SafeNpmInstallArgsOptions = {}
|
||||
return [
|
||||
"install",
|
||||
...(options.omitDev ? ["--omit=dev"] : []),
|
||||
...(options.omitPeer ? ["--omit=peer"] : []),
|
||||
...(options.legacyPeerDeps ? ["--legacy-peer-deps"] : []),
|
||||
...(options.loglevel ? [`--loglevel=${options.loglevel}`] : []),
|
||||
"--ignore-scripts",
|
||||
...(options.ignoreWorkspaces ? ["--workspaces=false"] : []),
|
||||
|
||||
@@ -360,6 +360,109 @@ describe("installPluginFromNpmSpec e2e", () => {
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("omits peers when installing beside an existing host-peer plugin", async () => {
|
||||
const rootDir = await makeTempDir("npm-plugin-sibling-peer-e2e");
|
||||
const npmRoot = path.join(rootDir, "managed-npm");
|
||||
const codexName = `codex-peer-plugin-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
||||
const opikName = `opik-peer-plugin-${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
||||
const registry = await startStaticRegistry([
|
||||
{
|
||||
packageName: codexName,
|
||||
latest: "1.0.0",
|
||||
versions: [
|
||||
await packPlugin({
|
||||
packageName: codexName,
|
||||
peerDependencies: { openclaw: ">=2026.5.5-beta.2" },
|
||||
peerDependenciesMeta: { openclaw: { optional: true } },
|
||||
pluginId: codexName,
|
||||
version: "1.0.0",
|
||||
rootDir,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
packageName: opikName,
|
||||
latest: "1.0.0",
|
||||
versions: [
|
||||
await packPlugin({
|
||||
packageName: opikName,
|
||||
peerDependencies: { openclaw: ">=2026.3.2" },
|
||||
peerDependenciesMeta: {},
|
||||
pluginId: opikName,
|
||||
version: "1.0.0",
|
||||
rootDir,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
packageName: "openclaw",
|
||||
latest: "2026.5.4",
|
||||
versions: [
|
||||
await packPlugin({
|
||||
packageName: "openclaw",
|
||||
pluginId: "registry-openclaw-copy",
|
||||
version: "2026.5.4",
|
||||
rootDir,
|
||||
}),
|
||||
],
|
||||
},
|
||||
]);
|
||||
process.env.NPM_CONFIG_REGISTRY = registry;
|
||||
process.env.npm_config_registry = registry;
|
||||
|
||||
await fs.mkdir(npmRoot, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "package.json"),
|
||||
`${JSON.stringify({ private: true, dependencies: { [codexName]: "1.0.0" } }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
["install", "--omit=peer", "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel=error"],
|
||||
{
|
||||
cwd: npmRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
NPM_CONFIG_REGISTRY: registry,
|
||||
NPM_CONFIG_LEGACY_PEER_DEPS: "false",
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: "false",
|
||||
npm_config_registry: registry,
|
||||
npm_config_legacy_peer_deps: "false",
|
||||
npm_config_strict_peer_deps: "false",
|
||||
},
|
||||
timeout: 120_000,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: `${opikName}@1.0.0`,
|
||||
npmDir: npmRoot,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
timeoutMs: 120_000,
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const lock = JSON.parse(await fs.readFile(path.join(npmRoot, "package-lock.json"), "utf8")) as {
|
||||
packages?: Record<string, unknown>;
|
||||
};
|
||||
expect(lock.packages?.["node_modules/openclaw"]).toBeUndefined();
|
||||
await expect(fs.lstat(path.join(npmRoot, "node_modules", "openclaw"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(
|
||||
fs
|
||||
.lstat(path.join(npmRoot, "node_modules", codexName, "node_modules", "openclaw"))
|
||||
.then((stat) => stat.isSymbolicLink()),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
fs
|
||||
.lstat(path.join(npmRoot, "node_modules", opikName, "node_modules", "openclaw"))
|
||||
.then((stat) => stat.isSymbolicLink()),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("relinks managed npm sibling openclaw peers after later plugin installs", async () => {
|
||||
const rootDir = await makeTempDir("npm-plugin-peer-e2e");
|
||||
const npmRoot = path.join(rootDir, "managed-npm");
|
||||
|
||||
@@ -50,6 +50,8 @@ function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string
|
||||
"npm",
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--omit=peer",
|
||||
"--legacy-peer-deps",
|
||||
"--loglevel=error",
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
|
||||
@@ -1372,6 +1372,8 @@ export async function installPluginFromNpmSpec(
|
||||
"npm",
|
||||
...createSafeNpmInstallArgs({
|
||||
omitDev: true,
|
||||
omitPeer: true,
|
||||
legacyPeerDeps: true,
|
||||
loglevel: "error",
|
||||
noAudit: true,
|
||||
noFund: true,
|
||||
@@ -1382,7 +1384,11 @@ export async function installPluginFromNpmSpec(
|
||||
{
|
||||
cwd: npmRoot,
|
||||
timeoutMs: Math.max(timeoutMs, 300_000),
|
||||
env: createSafeNpmInstallEnv(process.env, { packageLock: true, quiet: true }),
|
||||
env: createSafeNpmInstallEnv(process.env, {
|
||||
legacyPeerDeps: true,
|
||||
packageLock: true,
|
||||
quiet: true,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (install.code !== 0) {
|
||||
|
||||
Reference in New Issue
Block a user