mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
fix(plugins): repair stale managed openclaw peers
This commit is contained in:
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/update: treat official externalized bundled npm migrations and ClawHub-to-npm fallbacks as trusted source-linked installs, so prerelease-only official plugin packages can migrate from bundled builds without being rejected as unsafe prerelease resolutions. Thanks @vincentkoc.
|
||||
- Plugins/update: move ClawHub-preferred externalized plugin installs back to ClawHub after an earlier npm fallback once the ClawHub package becomes available. Thanks @vincentkoc.
|
||||
- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc.
|
||||
- Plugins/update: repair stale managed npm-root `openclaw` peer packages before plugin installs, so beta-channel official plugin updates are not downgraded by old core package-lock state. Thanks @vincentkoc.
|
||||
- Plugins/update: make package upgrades swap pnpm/npm-prefix installs cleanly, keep legacy plugin install runtime chunks working, and on the beta channel fall back default-line npm plugins to default/latest when plugin beta releases are missing or fail install validation. Thanks @vincentkoc and @joshavant.
|
||||
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
|
||||
- Sandbox/Windows: accept drive-absolute Docker bind sources while keeping sandbox blocked-path and allowed-root policy comparisons Windows-case-insensitive. (#42174) Thanks @6607changchun.
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
repairManagedNpmRootOpenClawPeer,
|
||||
removeManagedNpmRootDependency,
|
||||
readManagedNpmRootInstalledDependency,
|
||||
resolveManagedNpmRootDependencySpec,
|
||||
@@ -167,4 +168,80 @@ describe("managed npm root", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs stale managed openclaw peer state without dropping plugin packages", async () => {
|
||||
const npmRoot = await makeTempRoot();
|
||||
await fs.mkdir(path.join(npmRoot, "node_modules", "openclaw"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
dependencies: {
|
||||
openclaw: "2026.5.4",
|
||||
"@openclaw/discord": "2026.5.4",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "package-lock.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": {
|
||||
dependencies: {
|
||||
openclaw: "2026.5.4",
|
||||
"@openclaw/discord": "2026.5.4",
|
||||
},
|
||||
},
|
||||
"node_modules/openclaw": {
|
||||
version: "2026.5.4",
|
||||
},
|
||||
"node_modules/@openclaw/discord": {
|
||||
version: "2026.5.4",
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
openclaw: {
|
||||
version: "2026.5.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "node_modules", "openclaw", "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: "2026.5.4" })}\n`,
|
||||
);
|
||||
|
||||
await expect(repairManagedNpmRootOpenClawPeer({ npmRoot })).resolves.toBe(true);
|
||||
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(npmRoot, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
expect(manifest.dependencies).toEqual({
|
||||
"@openclaw/discord": "2026.5.4",
|
||||
});
|
||||
const lockfile = JSON.parse(
|
||||
await fs.readFile(path.join(npmRoot, "package-lock.json"), "utf8"),
|
||||
) as {
|
||||
packages?: Record<string, { dependencies?: Record<string, string>; version?: string }>;
|
||||
dependencies?: Record<string, unknown>;
|
||||
};
|
||||
expect(lockfile.packages?.[""]?.dependencies).toEqual({
|
||||
"@openclaw/discord": "2026.5.4",
|
||||
});
|
||||
expect(lockfile.packages?.["node_modules/openclaw"]).toBeUndefined();
|
||||
expect(lockfile.packages?.["node_modules/@openclaw/discord"]?.version).toBe("2026.5.4");
|
||||
expect(lockfile.dependencies?.openclaw).toBeUndefined();
|
||||
await expect(fs.lstat(path.join(npmRoot, "node_modules", "openclaw"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,12 @@ export type ManagedNpmRootInstalledDependency = {
|
||||
resolved?: string;
|
||||
};
|
||||
|
||||
type ManagedNpmRootLockfile = {
|
||||
packages?: Record<string, unknown>;
|
||||
dependencies?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -75,6 +81,78 @@ export async function upsertManagedNpmRootDependency(params: {
|
||||
await fs.writeFile(manifestPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
export async function repairManagedNpmRootOpenClawPeer(params: {
|
||||
npmRoot: string;
|
||||
}): Promise<boolean> {
|
||||
let changed = false;
|
||||
|
||||
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);
|
||||
if ("openclaw" in dependencies) {
|
||||
const { openclaw: _removed, ...nextDependencies } = dependencies;
|
||||
await fs.writeFile(
|
||||
manifestPath,
|
||||
`${JSON.stringify({ ...manifest, private: true, dependencies: nextDependencies }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const lockPath = path.join(params.npmRoot, "package-lock.json");
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(lockPath, "utf8")) as ManagedNpmRootLockfile;
|
||||
let lockChanged = false;
|
||||
if (isRecord(parsed.packages)) {
|
||||
const rootPackage = parsed.packages[""];
|
||||
if (isRecord(rootPackage) && isRecord(rootPackage.dependencies)) {
|
||||
const dependencies = { ...rootPackage.dependencies };
|
||||
if ("openclaw" in dependencies) {
|
||||
delete dependencies.openclaw;
|
||||
parsed.packages[""] = { ...rootPackage, dependencies };
|
||||
lockChanged = true;
|
||||
}
|
||||
}
|
||||
if ("node_modules/openclaw" in parsed.packages) {
|
||||
delete parsed.packages["node_modules/openclaw"];
|
||||
lockChanged = true;
|
||||
}
|
||||
}
|
||||
if (isRecord(parsed.dependencies) && "openclaw" in parsed.dependencies) {
|
||||
const dependencies = { ...parsed.dependencies };
|
||||
delete dependencies.openclaw;
|
||||
parsed.dependencies = dependencies;
|
||||
lockChanged = true;
|
||||
}
|
||||
if (lockChanged) {
|
||||
await fs.writeFile(lockPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
||||
changed = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const openclawPackageDir = path.join(params.npmRoot, "node_modules", "openclaw");
|
||||
const openclawPackageDirExists = await fs
|
||||
.lstat(openclawPackageDir)
|
||||
.then(() => true)
|
||||
.catch((err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (openclawPackageDirExists) {
|
||||
await fs.rm(openclawPackageDir, { recursive: true, force: true });
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
export async function readManagedNpmRootInstalledDependency(params: {
|
||||
npmRoot: string;
|
||||
packageName: string;
|
||||
|
||||
@@ -507,6 +507,94 @@ describe("installPluginFromNpmSpec", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("repairs stale managed openclaw root packages before npm plugin installs", async () => {
|
||||
const stateDir = suiteTempRootTracker.makeTempDir();
|
||||
const npmRoot = path.join(stateDir, "npm");
|
||||
fs.mkdirSync(path.join(npmRoot, "node_modules", "openclaw"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(npmRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
dependencies: {
|
||||
openclaw: "2026.5.4",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(npmRoot, "package-lock.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": {
|
||||
dependencies: {
|
||||
openclaw: "2026.5.4",
|
||||
},
|
||||
},
|
||||
"node_modules/openclaw": {
|
||||
version: "2026.5.4",
|
||||
resolved: "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.4.tgz",
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
openclaw: {
|
||||
version: "2026.5.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(npmRoot, "node_modules", "openclaw", "package.json"),
|
||||
JSON.stringify({
|
||||
name: "openclaw",
|
||||
version: "2026.5.4",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
mockNpmViewAndInstall({
|
||||
spec: "@openclaw/discord@beta",
|
||||
packageName: "@openclaw/discord",
|
||||
version: "2026.5.5-beta.1",
|
||||
pluginId: "discord",
|
||||
npmRoot,
|
||||
peerDependencies: { openclaw: ">=2026.5.5-beta.1" },
|
||||
expectedDependencySpec: "2026.5.5-beta.1",
|
||||
});
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: "@openclaw/discord@beta",
|
||||
npmDir: npmRoot,
|
||||
logger: { info: () => {}, warn: () => {} },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(npmRoot, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
expect(manifest.dependencies).not.toHaveProperty("openclaw");
|
||||
expect(manifest.dependencies).toMatchObject({
|
||||
"@openclaw/discord": "2026.5.5-beta.1",
|
||||
});
|
||||
const lockfile = JSON.parse(
|
||||
fs.readFileSync(path.join(npmRoot, "package-lock.json"), "utf8"),
|
||||
) as {
|
||||
packages?: Record<string, unknown>;
|
||||
dependencies?: Record<string, unknown>;
|
||||
};
|
||||
expect(lockfile.packages?.["node_modules/openclaw"]).toBeUndefined();
|
||||
expect(lockfile.dependencies?.openclaw).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows npm-spec installs with dangerous code patterns when forced unsafe install is set", async () => {
|
||||
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
|
||||
const warnings: string[] = [];
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { resolveNpmIntegrityDriftWithDefaultMessage } from "../infra/npm-integrity.js";
|
||||
import {
|
||||
readManagedNpmRootInstalledDependency,
|
||||
repairManagedNpmRootOpenClawPeer,
|
||||
removeManagedNpmRootDependency,
|
||||
resolveManagedNpmRootDependencySpec,
|
||||
upsertManagedNpmRootDependency,
|
||||
@@ -1335,6 +1336,12 @@ export async function installPluginFromNpmSpec(
|
||||
}
|
||||
|
||||
logger.info?.(`Installing ${spec} into ${npmRoot}…`);
|
||||
if (parsedSpec.name !== "openclaw") {
|
||||
const repairedOpenClawPeer = await repairManagedNpmRootOpenClawPeer({ npmRoot });
|
||||
if (repairedOpenClawPeer) {
|
||||
logger.info?.(`Repaired stale openclaw peer dependency in ${npmRoot}`);
|
||||
}
|
||||
}
|
||||
await upsertManagedNpmRootDependency({
|
||||
npmRoot,
|
||||
packageName: parsedSpec.name,
|
||||
|
||||
Reference in New Issue
Block a user