mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(plugins): fall back from invalid beta npm updates
This commit is contained in:
@@ -42,6 +42,11 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
|
||||
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. 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.
|
||||
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
|
||||
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
|
||||
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
|
||||
- Plugins/ClawHub: annotate 429 errors from ClawHub with the reset window from `RateLimit-Reset`/`Retry-After` and append a `Sign in for higher rate limits.` hint when the request was unauthenticated, so users can see when downloads will recover and how to lift the cap. Thanks @romneyda.
|
||||
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
|
||||
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
|
||||
|
||||
@@ -67,6 +72,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/status: label Linux managed gateway services as `systemd user`, making status output explicit about the user-service scope instead of implying a system-level unit. Thanks @vincentkoc.
|
||||
- Plugins/install: remove the previous managed plugin directory when a reinstall switches sources, so stale ClawHub and npm copies no longer keep duplicate plugin ids in discovery after the new install wins. Thanks @vincentkoc.
|
||||
- Plugins/install: let official plugin reinstall recovery repair source-only installed runtime shadows, so `openclaw plugins install npm:@openclaw/discord --force` can replace the bad package instead of stopping at stale config validation. Thanks @vincentkoc.
|
||||
- CLI/update: stage pnpm-detected npm-layout global package updates through a clean npm prefix swap, keep plugin install runtime imports behind a stable alias, and ship legacy install-runtime aliases back to `2026.3.22`, preventing stale overlay chunks from breaking plugin post-update sync. Thanks @vincentkoc.
|
||||
- Plugins/commands: allow the official ClawHub Codex plugin package to keep reserved `/codex` command ownership, matching the existing npm-managed Codex package behavior. Thanks @vincentkoc.
|
||||
- Auth/OpenAI Codex: rewrite invalidated per-agent Codex auth-order and session profile overrides toward a healthy relogin profile, so revoked OAuth accounts do not stay pinned after signing in again. Thanks @BunsDev.
|
||||
- Plugins/commands: scope QQBot framework slash commands to the QQBot channel so `/bot-*` command handlers and native specs do not leak onto unrelated chat surfaces. Thanks @vincentkoc.
|
||||
|
||||
@@ -168,8 +168,9 @@ manually.
|
||||
|
||||
On the beta update channel, tracked npm and ClawHub plugin installs that follow
|
||||
the default/latest line try a plugin `@beta` release first. If the plugin has no
|
||||
beta release, OpenClaw falls back to the recorded default/latest spec. Exact
|
||||
versions and explicit tags are not rewritten.
|
||||
beta release, OpenClaw falls back to the recorded default/latest spec. For npm
|
||||
plugins, OpenClaw also falls back when the beta package exists but fails install
|
||||
validation. Exact versions and explicit tags are not rewritten.
|
||||
|
||||
<Warning>
|
||||
If an exact pinned npm plugin update resolves to an artifact whose integrity differs from the stored install record, `openclaw update` aborts that plugin artifact update instead of installing it. Reinstall or update the plugin explicitly only after verifying that you trust the new artifact.
|
||||
|
||||
@@ -92,7 +92,9 @@ when it was previously pinned to an exact version or tag.
|
||||
When `openclaw update` runs on the beta channel, default-line npm and ClawHub
|
||||
plugin records try the matching plugin `@beta` release first. If that beta
|
||||
release does not exist, OpenClaw falls back to the recorded default/latest spec.
|
||||
Exact versions and explicit tags such as `@rc` or `@beta` are preserved.
|
||||
For npm plugins, OpenClaw also falls back when the beta package exists but fails
|
||||
install validation. Exact versions and explicit tags such as `@rc` or `@beta`
|
||||
are preserved.
|
||||
|
||||
## Uninstall plugins
|
||||
|
||||
|
||||
@@ -1293,6 +1293,87 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the default npm spec when the beta package exists but is invalid", async () => {
|
||||
installPluginFromNpmSpecMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error: "Installed plugin package uses a TypeScript entry without compiled runtime output.",
|
||||
})
|
||||
.mockResolvedValueOnce(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
targetDir: "/tmp/openclaw-codex-app-server",
|
||||
version: "0.2.6",
|
||||
npmResolution: {
|
||||
name: "openclaw-codex-app-server",
|
||||
version: "0.2.6",
|
||||
resolvedSpec: "openclaw-codex-app-server@0.2.6",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const warnMessages: string[] = [];
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config: createCodexAppServerInstallConfig({
|
||||
spec: "openclaw-codex-app-server",
|
||||
}),
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
updateChannel: "beta",
|
||||
logger: { warn: (msg) => warnMessages.push(msg) },
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
spec: "openclaw-codex-app-server@beta",
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
spec: "openclaw-codex-app-server",
|
||||
}),
|
||||
);
|
||||
expect(warnMessages).toEqual([expect.stringContaining("failed beta npm update")]);
|
||||
expectCodexAppServerInstallState({
|
||||
result,
|
||||
spec: "openclaw-codex-app-server",
|
||||
version: "0.2.6",
|
||||
resolvedSpec: "openclaw-codex-app-server@0.2.6",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the fallback npm spec when beta fallback also fails", async () => {
|
||||
installPluginFromNpmSpecMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error: "Installed plugin package uses a TypeScript entry without compiled runtime output.",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
code: "npm_package_not_found",
|
||||
error: "npm package not found",
|
||||
});
|
||||
|
||||
const result = await updateNpmInstalledPlugins({
|
||||
config: createCodexAppServerInstallConfig({
|
||||
spec: "openclaw-codex-app-server",
|
||||
}),
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
updateChannel: "beta",
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledTimes(2);
|
||||
expect(result.outcomes).toEqual([
|
||||
{
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
status: "error",
|
||||
message:
|
||||
"Failed to update openclaw-codex-app-server: npm package not found for openclaw-codex-app-server.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves explicit npm tags when updating on the beta channel", async () => {
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
|
||||
@@ -431,13 +431,31 @@ function shouldFallbackBetaClawHubUpdate(result: { ok: false; code?: string }):
|
||||
return shouldFallbackClawHubBridgeToNpm(result);
|
||||
}
|
||||
|
||||
function shouldFallbackBetaNpmUpdate(result: { ok: false; code?: string; error: string }): boolean {
|
||||
if (result.code === PLUGIN_INSTALL_ERROR_CODE.NPM_PACKAGE_NOT_FOUND) {
|
||||
return true;
|
||||
function describeBetaNpmFallback(params: {
|
||||
pluginId: string;
|
||||
betaSpec: string | undefined;
|
||||
fallbackSpec: string;
|
||||
result: { ok: false; code?: string; error: string };
|
||||
}): string {
|
||||
const betaSpec = params.betaSpec ?? "the beta npm release";
|
||||
const missingBeta =
|
||||
params.result.code === PLUGIN_INSTALL_ERROR_CODE.NPM_PACKAGE_NOT_FOUND ||
|
||||
/\b(ETARGET|notarget)\b|No matching version found|dist-tag|tag .*not found/i.test(
|
||||
params.result.error,
|
||||
);
|
||||
const reason = missingBeta ? "has no beta npm release" : "failed beta npm update";
|
||||
return `Plugin "${params.pluginId}" ${reason} for ${betaSpec}; falling back to ${params.fallbackSpec}.`;
|
||||
}
|
||||
|
||||
function npmUpdateFailureSpec(params: {
|
||||
effectiveSpec: string | undefined;
|
||||
fallbackSpec: string | undefined;
|
||||
usedFallback: boolean;
|
||||
}): string {
|
||||
if (params.usedFallback && params.fallbackSpec) {
|
||||
return params.fallbackSpec;
|
||||
}
|
||||
return /\b(ETARGET|notarget)\b|No matching version found|dist-tag|tag .*not found/i.test(
|
||||
result.error,
|
||||
);
|
||||
return params.effectiveSpec ?? params.fallbackSpec ?? "unknown";
|
||||
}
|
||||
|
||||
function isDefaultNpmSpecForBetaUpdate(spec: string): { name: string } | null {
|
||||
@@ -1026,15 +1044,17 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!probe.ok &&
|
||||
record.source === "npm" &&
|
||||
npmSpecs?.fallbackSpec &&
|
||||
shouldFallbackBetaNpmUpdate(probe)
|
||||
) {
|
||||
let usedNpmFallback = false;
|
||||
if (!probe.ok && record.source === "npm" && npmSpecs?.fallbackSpec) {
|
||||
logger.warn?.(
|
||||
`Plugin "${pluginId}" has no beta npm release for ${npmSpecs.fallbackLabel ?? effectiveSpec}; falling back to ${npmSpecs.fallbackSpec}.`,
|
||||
describeBetaNpmFallback({
|
||||
pluginId,
|
||||
betaSpec: npmSpecs.fallbackLabel ?? effectiveSpec,
|
||||
fallbackSpec: npmSpecs.fallbackSpec,
|
||||
result: probe,
|
||||
}),
|
||||
);
|
||||
usedNpmFallback = true;
|
||||
probe = await installPluginFromNpmSpec({
|
||||
spec: npmSpecs.fallbackSpec,
|
||||
mode: "update",
|
||||
@@ -1083,7 +1103,11 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
record.source === "npm"
|
||||
? formatNpmInstallFailure({
|
||||
pluginId,
|
||||
spec: effectiveSpec!,
|
||||
spec: npmUpdateFailureSpec({
|
||||
effectiveSpec,
|
||||
fallbackSpec: npmSpecs?.fallbackSpec,
|
||||
usedFallback: usedNpmFallback,
|
||||
}),
|
||||
phase: "check",
|
||||
result: probe,
|
||||
})
|
||||
@@ -1207,15 +1231,17 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!result.ok &&
|
||||
record.source === "npm" &&
|
||||
npmSpecs?.fallbackSpec &&
|
||||
shouldFallbackBetaNpmUpdate(result)
|
||||
) {
|
||||
let usedNpmFallback = false;
|
||||
if (!result.ok && record.source === "npm" && npmSpecs?.fallbackSpec) {
|
||||
logger.warn?.(
|
||||
`Plugin "${pluginId}" has no beta npm release for ${npmSpecs.fallbackLabel ?? effectiveSpec}; falling back to ${npmSpecs.fallbackSpec}.`,
|
||||
describeBetaNpmFallback({
|
||||
pluginId,
|
||||
betaSpec: npmSpecs.fallbackLabel ?? effectiveSpec,
|
||||
fallbackSpec: npmSpecs.fallbackSpec,
|
||||
result,
|
||||
}),
|
||||
);
|
||||
usedNpmFallback = true;
|
||||
result = await installPluginFromNpmSpec({
|
||||
spec: npmSpecs.fallbackSpec,
|
||||
mode: "update",
|
||||
@@ -1262,7 +1288,11 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
record.source === "npm"
|
||||
? formatNpmInstallFailure({
|
||||
pluginId,
|
||||
spec: effectiveSpec!,
|
||||
spec: npmUpdateFailureSpec({
|
||||
effectiveSpec,
|
||||
fallbackSpec: npmSpecs?.fallbackSpec,
|
||||
usedFallback: usedNpmFallback,
|
||||
}),
|
||||
phase: "update",
|
||||
result: result,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user