mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-19 07:42:04 +00:00
fix(plugins): preserve host package during managed peer repair
This commit is contained in:
parent
56aec53dde
commit
89a9b4e75a
8 changed files with 187 additions and 15 deletions
|
|
@ -22,6 +22,8 @@ Docs: https://docs.openclaw.ai
|
|||
- Agents/OpenAI Responses: clamp `input_tokens - cached_tokens` at zero and reconstruct `totalTokens` from input + output + cached components so Responses-API streams report consistent usage when providers under-report `input_tokens` relative to `cached_tokens`.
|
||||
- Plugins: reject malformed `package.json` `openclaw.extensions` metadata during install, discovery, and post-update payload smoke instead of silently dropping invalid entries.
|
||||
- Media/files: sniff `input_file` bytes before trusting declared MIME headers, rejecting spoofed image or zip payloads before they become agent-visible text.
|
||||
- Plugins/dependencies: scrub stale managed-root `openclaw` ownership metadata without deleting a linked active host package, preventing plugin installs from downgrading npm-global hosts. Fixes #79462. Thanks @lisandromachado.
|
||||
- Gateway/update: keep shutdown hook-runner imports on a stable dist entry and ship a legacy chunk alias so package swaps do not strand running gateways on missing shutdown chunks. Fixes #81819. Thanks @najef1979-code.
|
||||
- Config persistence: ignore malformed array/scalar auth profile, cron job state, and session store entries instead of hydrating them into numeric profile ids, crashed cron rows, or invalid session records.
|
||||
- Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling.
|
||||
- Providers/images: reject malformed successful OpenAI-compatible, OpenAI, Google, fal, and OpenRouter image responses with provider-owned errors instead of raw shape failures, silent invalid base64 skips, or empty image results.
|
||||
|
|
|
|||
|
|
@ -234,12 +234,16 @@ export class TelegramPollingSession {
|
|||
if (this.#deliveryDrainInFlight) {
|
||||
return;
|
||||
}
|
||||
if (!this.opts.config) {
|
||||
return;
|
||||
}
|
||||
this.#deliveryDrainInFlight = true;
|
||||
const accountId = normalizeTelegramAccountId(this.opts.accountId);
|
||||
const cfg = this.opts.config;
|
||||
void drainPendingDeliveries({
|
||||
drainKey: `telegram:${accountId}`,
|
||||
logLabel: "Telegram reconnect drain",
|
||||
cfg: this.opts.config,
|
||||
cfg,
|
||||
log: {
|
||||
info: (message) => this.opts.log(`[telegram][diag] ${message}`),
|
||||
warn: (message) => this.opts.log(`[telegram] ${message}`),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [
|
|||
// gateway may resolve these only after an npm package tree replacement.
|
||||
["server-close-DsVPJDIx.js", "server-close.runtime.js"],
|
||||
["server-close-DvAvfgr8.js", "server-close.runtime.js"],
|
||||
// v2026.5.12-beta.8 gateway shutdown hook chunks.
|
||||
["hook-runner-global-B8rMIo8I.js", "plugins/hook-runner-global.js"],
|
||||
// v2026.5.3 beta reply-dispatch lazy chunks.
|
||||
["provider-dispatcher-6EQEtc-t.js", "provider-dispatcher.runtime.js"],
|
||||
["provider-dispatcher-BpL2E92x.js", "provider-dispatcher.runtime.js"],
|
||||
|
|
|
|||
|
|
@ -919,4 +919,120 @@ describe("managed npm root", () => {
|
|||
fs.readFile(path.join(hostPackageRoot, "package.json"), "utf8"),
|
||||
).resolves.toContain("2026.5.12-beta.6");
|
||||
});
|
||||
|
||||
it("scrubs managed ownership metadata without deleting a linked active host package", async () => {
|
||||
const npmRoot = await makeTempRoot();
|
||||
const hostPackageRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-host-package-"));
|
||||
tempDirs.push(hostPackageRoot);
|
||||
await fs.mkdir(path.join(npmRoot, "node_modules", ".bin"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(hostPackageRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: "2026.5.12-beta.6" })}\n`,
|
||||
);
|
||||
await fs.symlink(hostPackageRoot, path.join(npmRoot, "node_modules", "openclaw"), "dir");
|
||||
await fs.writeFile(path.join(npmRoot, "node_modules", ".bin", "openclaw"), "shim");
|
||||
await fs.writeFile(path.join(npmRoot, "node_modules", ".bin", "openclaw.cmd"), "cmd shim");
|
||||
await fs.writeFile(path.join(npmRoot, "node_modules", ".bin", "openclaw.ps1"), "ps1 shim");
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "node_modules", ".package-lock.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"node_modules/openclaw": {
|
||||
version: "2026.5.12-beta.6",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
dependencies: {
|
||||
openclaw: "2026.5.12-beta.6",
|
||||
"@xdarkicex/openclaw-memory-libravdb": "1.4.69",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(npmRoot, "package-lock.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": {
|
||||
dependencies: {
|
||||
openclaw: "2026.5.12-beta.6",
|
||||
"@xdarkicex/openclaw-memory-libravdb": "1.4.69",
|
||||
},
|
||||
},
|
||||
"node_modules/openclaw": {
|
||||
version: "2026.5.12-beta.6",
|
||||
},
|
||||
"node_modules/@xdarkicex/openclaw-memory-libravdb": {
|
||||
version: "1.4.69",
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
openclaw: {
|
||||
version: "2026.5.12-beta.6",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
const runCommand = vi.fn().mockResolvedValue(successfulSpawn);
|
||||
await expect(
|
||||
repairManagedNpmRootOpenClawPeer({
|
||||
npmRoot,
|
||||
packageRoot: hostPackageRoot,
|
||||
runCommand,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(runCommand).not.toHaveBeenCalled();
|
||||
await expect(fs.realpath(path.join(npmRoot, "node_modules", "openclaw"))).resolves.toBe(
|
||||
await fs.realpath(hostPackageRoot),
|
||||
);
|
||||
await expect(
|
||||
fs.readFile(path.join(hostPackageRoot, "package.json"), "utf8"),
|
||||
).resolves.toContain("2026.5.12-beta.6");
|
||||
|
||||
const manifest = JSON.parse(await fs.readFile(path.join(npmRoot, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
};
|
||||
expect(manifest.dependencies).toEqual({
|
||||
"@xdarkicex/openclaw-memory-libravdb": "1.4.69",
|
||||
});
|
||||
|
||||
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({
|
||||
"@xdarkicex/openclaw-memory-libravdb": "1.4.69",
|
||||
});
|
||||
expect(lockfile.packages?.["node_modules/openclaw"]).toBeUndefined();
|
||||
expect(lockfile.packages?.["node_modules/@xdarkicex/openclaw-memory-libravdb"]?.version).toBe(
|
||||
"1.4.69",
|
||||
);
|
||||
expect(lockfile.dependencies?.openclaw).toBeUndefined();
|
||||
for (const binName of ["openclaw", "openclaw.cmd", "openclaw.ps1"]) {
|
||||
await expectPathMissing(path.join(npmRoot, "node_modules", ".bin", binName));
|
||||
}
|
||||
await expectPathMissing(path.join(npmRoot, "node_modules", ".package-lock.json"));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Stats } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
|
@ -52,6 +53,8 @@ type ManagedNpmRootLogger = {
|
|||
|
||||
type ManagedNpmRootRunCommand = typeof runCommandWithTimeout;
|
||||
|
||||
type ManagedNpmRootOpenClawHostState = "none" | "managed-active-host" | "linked-active-host";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -758,12 +761,11 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
|
|||
}): Promise<boolean> {
|
||||
await fs.mkdir(params.npmRoot, { recursive: true });
|
||||
|
||||
if (
|
||||
await managedNpmRootOpenClawPackageIsActiveHost({
|
||||
npmRoot: params.npmRoot,
|
||||
packageRoot: params.packageRoot,
|
||||
})
|
||||
) {
|
||||
const activeHostState = await readManagedNpmRootOpenClawHostState({
|
||||
npmRoot: params.npmRoot,
|
||||
packageRoot: params.packageRoot,
|
||||
});
|
||||
if (activeHostState === "managed-active-host") {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -773,10 +775,19 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
|
|||
const hasManifestDependency = "openclaw" in dependencies;
|
||||
const hasLockDependency = await managedNpmRootLockfileHasOpenClawPeer(params.npmRoot);
|
||||
const hasPackageDir = await pathExists(path.join(params.npmRoot, "node_modules", "openclaw"));
|
||||
if (!hasManifestDependency && !hasLockDependency && !hasPackageDir) {
|
||||
const preserveActiveHostLink = activeHostState === "linked-active-host";
|
||||
if (!hasManifestDependency && !hasLockDependency && (!hasPackageDir || preserveActiveHostLink)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preserveActiveHostLink) {
|
||||
await scrubManagedNpmRootOpenClawPeer({
|
||||
npmRoot: params.npmRoot,
|
||||
preservePackageDir: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const command = params.runCommand ?? runCommandWithTimeout;
|
||||
const npmArgs = hasManifestDependency
|
||||
? [
|
||||
|
|
@ -823,10 +834,10 @@ export async function repairManagedNpmRootOpenClawPeer(params: {
|
|||
return true;
|
||||
}
|
||||
|
||||
async function managedNpmRootOpenClawPackageIsActiveHost(params: {
|
||||
async function readManagedNpmRootOpenClawHostState(params: {
|
||||
npmRoot: string;
|
||||
packageRoot?: string | null;
|
||||
}): Promise<boolean> {
|
||||
}): Promise<ManagedNpmRootOpenClawHostState> {
|
||||
const packageRoot =
|
||||
params.packageRoot === undefined
|
||||
? resolveOpenClawPackageRootSync({
|
||||
|
|
@ -836,15 +847,19 @@ async function managedNpmRootOpenClawPackageIsActiveHost(params: {
|
|||
})
|
||||
: params.packageRoot;
|
||||
if (!packageRoot) {
|
||||
return false;
|
||||
return "none";
|
||||
}
|
||||
|
||||
const managedOpenClawPackageDir = path.join(params.npmRoot, "node_modules", "openclaw");
|
||||
const [hostPackageRoot, managedPackageRoot] = await Promise.all([
|
||||
const [hostPackageRoot, managedPackageRoot, managedPackageStat] = await Promise.all([
|
||||
realpathIfExists(packageRoot),
|
||||
realpathIfExists(managedOpenClawPackageDir),
|
||||
lstatIfExists(managedOpenClawPackageDir),
|
||||
]);
|
||||
return hostPackageRoot !== null && hostPackageRoot === managedPackageRoot;
|
||||
if (hostPackageRoot === null || hostPackageRoot !== managedPackageRoot) {
|
||||
return "none";
|
||||
}
|
||||
return managedPackageStat?.isSymbolicLink() ? "linked-active-host" : "managed-active-host";
|
||||
}
|
||||
|
||||
async function managedNpmRootLockfileHasOpenClawPeer(npmRoot: string): Promise<boolean> {
|
||||
|
|
@ -884,6 +899,17 @@ async function realpathIfExists(filePath: string): Promise<string | null> {
|
|||
}
|
||||
}
|
||||
|
||||
async function lstatIfExists(filePath: string): Promise<Stats | null> {
|
||||
try {
|
||||
return await fs.lstat(filePath);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(filePath: string): Promise<boolean> {
|
||||
return await fs
|
||||
.lstat(filePath)
|
||||
|
|
@ -896,7 +922,10 @@ async function pathExists(filePath: string): Promise<boolean> {
|
|||
});
|
||||
}
|
||||
|
||||
async function scrubManagedNpmRootOpenClawPeer(params: { npmRoot: string }): Promise<void> {
|
||||
async function scrubManagedNpmRootOpenClawPeer(params: {
|
||||
npmRoot: string;
|
||||
preservePackageDir?: boolean;
|
||||
}): Promise<void> {
|
||||
const manifestPath = path.join(params.npmRoot, "package.json");
|
||||
const manifest = await readManagedNpmRootManifest(manifestPath);
|
||||
const dependencies = readDependencyRecord(manifest.dependencies);
|
||||
|
|
@ -944,7 +973,7 @@ async function scrubManagedNpmRootOpenClawPeer(params: { npmRoot: string }): Pro
|
|||
}
|
||||
|
||||
const openclawPackageDir = path.join(params.npmRoot, "node_modules", "openclaw");
|
||||
if (await pathExists(openclawPackageDir)) {
|
||||
if (!params.preservePackageDir && (await pathExists(openclawPackageDir))) {
|
||||
await fs.rm(openclawPackageDir, { recursive: true, force: true });
|
||||
}
|
||||
const binDir = path.join(params.npmRoot, "node_modules", ".bin");
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ describe("tsdown config", () => {
|
|||
"index",
|
||||
"commands/status.summary.runtime",
|
||||
"provider-dispatcher.runtime",
|
||||
"plugins/hook-runner-global",
|
||||
"plugins/provider-discovery.runtime",
|
||||
"plugins/provider-runtime.runtime",
|
||||
"plugins/runtime/index",
|
||||
|
|
@ -138,6 +139,14 @@ describe("tsdown config", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("keeps gateway shutdown hook runner behind one stable dist entry", () => {
|
||||
const distGraph = requireUnifiedDistGraph();
|
||||
|
||||
expect(entrySources(distGraph)["plugins/hook-runner-global"]).toBe(
|
||||
"src/plugins/hook-runner-global.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps Telegram ingress worker behind one root stable dist entry", () => {
|
||||
const distGraph = requireUnifiedDistGraph();
|
||||
|
||||
|
|
|
|||
|
|
@ -688,12 +688,18 @@ describe("runtime postbuild static assets", () => {
|
|||
it("writes compatibility aliases for previous gateway shutdown chunk names", async () => {
|
||||
const rootDir = createTempDir("openclaw-runtime-postbuild-");
|
||||
const distDir = path.join(rootDir, "dist");
|
||||
await fs.mkdir(path.join(distDir, "plugins"), { recursive: true });
|
||||
await fs.mkdir(distDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "server-close.runtime.js"),
|
||||
'export * from "./server-close.runtime-NewHash.js";\n',
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "plugins", "hook-runner-global.js"),
|
||||
"export const runGlobalHook = true;\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
writeLegacyRootRuntimeCompatAliases({ rootDir });
|
||||
|
||||
|
|
@ -703,6 +709,9 @@ describe("runtime postbuild static assets", () => {
|
|||
expect(await fs.readFile(path.join(distDir, "server-close-DvAvfgr8.js"), "utf8")).toBe(
|
||||
'export * from "./server-close.runtime.js";\n',
|
||||
);
|
||||
expect(await fs.readFile(path.join(distDir, "hook-runner-global-B8rMIo8I.js"), "utf8")).toBe(
|
||||
'export * from "./plugins/hook-runner-global.js";\n',
|
||||
);
|
||||
});
|
||||
|
||||
it("writes compatibility aliases for previous tool and ACP manager chunk names", async () => {
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ function buildCoreDistEntries(): Record<string, string> {
|
|||
"cli/gateway-lifecycle.runtime": "src/cli/gateway-cli/lifecycle.runtime.ts",
|
||||
"provider-dispatcher.runtime": "src/auto-reply/reply/provider-dispatcher.runtime.ts",
|
||||
"server-close.runtime": "src/gateway/server-close.runtime.ts",
|
||||
"plugins/hook-runner-global": "src/plugins/hook-runner-global.ts",
|
||||
"plugins/memory-state": "src/plugins/memory-state.ts",
|
||||
"subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts",
|
||||
"task-registry-control.runtime": "src/tasks/task-registry-control.runtime.ts",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue