mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix(plugins): trust official externalized npm installs
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/performance: lazy-load the heavy cron runtime after the rest of Gateway startup, defer restart-sentinel refresh after readiness, and let the Gateway startup benchmark write per-run V8 CPU profiles with `--cpu-prof-dir`.
|
||||
- Gateway/performance: keep raw channel-config schema parsing from discovering bundled plugin runtime metadata, and add `pnpm gateway:watch --benchmark-no-force` for profiling startup without the default port cleanup.
|
||||
- Plugins/onboarding: let Manual setup install optional official plugins, including ClawHub-backed diagnostics with npm fallback, and expose the external Codex plugin as a selectable provider setup choice. Thanks @vincentkoc.
|
||||
- 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/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
|
||||
- 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.
|
||||
|
||||
@@ -2079,6 +2079,53 @@ describe("syncPluginsForUpdateChannel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("marks official externalized bundled npm installs as trusted", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "voice-call",
|
||||
targetDir: "/tmp/openclaw-plugins/voice-call",
|
||||
version: "0.0.2-beta.1",
|
||||
}),
|
||||
);
|
||||
|
||||
await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "voice-call",
|
||||
npmSpec: "@openclaw/voice-call",
|
||||
channelIds: ["voice-call"],
|
||||
},
|
||||
],
|
||||
config: {
|
||||
channels: {
|
||||
"voice-call": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
load: { paths: [appBundledPluginRoot("voice-call")] },
|
||||
installs: {
|
||||
"voice-call": {
|
||||
source: "path",
|
||||
sourcePath: appBundledPluginRoot("voice-call"),
|
||||
installPath: appBundledPluginRoot("voice-call"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/voice-call",
|
||||
expectedPluginId: "voice-call",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("installs a ClawHub-preferred externalized bundled plugin", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
installPluginFromClawHubMock.mockResolvedValue(
|
||||
@@ -2229,6 +2276,60 @@ describe("syncPluginsForUpdateChannel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("marks official externalized ClawHub-to-npm fallbacks as trusted", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
installPluginFromClawHubMock.mockResolvedValue({
|
||||
ok: false,
|
||||
code: "package_not_found",
|
||||
error: "Package not found on ClawHub.",
|
||||
});
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "voice-call",
|
||||
targetDir: "/tmp/openclaw-plugins/voice-call",
|
||||
version: "0.0.2-beta.1",
|
||||
}),
|
||||
);
|
||||
|
||||
await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "voice-call",
|
||||
preferredSource: "clawhub",
|
||||
clawhubSpec: "clawhub:@openclaw/voice-call",
|
||||
npmSpec: "@openclaw/voice-call",
|
||||
channelIds: ["voice-call"],
|
||||
},
|
||||
],
|
||||
config: {
|
||||
channels: {
|
||||
"voice-call": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
load: { paths: [appBundledPluginRoot("voice-call")] },
|
||||
installs: {
|
||||
"voice-call": {
|
||||
source: "path",
|
||||
sourcePath: appBundledPluginRoot("voice-call"),
|
||||
installPath: appBundledPluginRoot("voice-call"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/voice-call",
|
||||
expectedPluginId: "voice-call",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed without npm fallback when ClawHub returns integrity drift", async () => {
|
||||
resolveBundledPluginSourcesMock.mockReturnValue(new Map());
|
||||
installPluginFromClawHubMock.mockResolvedValue({
|
||||
|
||||
@@ -477,6 +477,21 @@ function isTrustedSourceLinkedOfficialNpmUpdate(params: {
|
||||
return recordedPackageNames.includes(officialPackageName);
|
||||
}
|
||||
|
||||
function isTrustedSourceLinkedOfficialBridgeNpmInstall(params: {
|
||||
targetPluginId: string;
|
||||
npmSpec: string | undefined;
|
||||
}): boolean {
|
||||
const entry = getOfficialExternalPluginCatalogEntry(params.targetPluginId);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
const officialPackageName = resolveNpmSpecPackageName(
|
||||
resolveOfficialExternalPluginInstall(entry)?.npmSpec,
|
||||
);
|
||||
const requestedPackageName = resolveNpmSpecPackageName(params.npmSpec);
|
||||
return Boolean(officialPackageName && requestedPackageName === officialPackageName);
|
||||
}
|
||||
|
||||
function resolveNpmUpdateSpecs(params: {
|
||||
record: PluginInstallRecord;
|
||||
specOverride?: string;
|
||||
@@ -1427,6 +1442,10 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
const preferredSource = getExternalizedBundledPluginPreferredSource(bridge);
|
||||
const npmSpec = getExternalizedBundledPluginNpmSpec(bridge);
|
||||
const clawhubSpec = getExternalizedBundledPluginClawHubSpec(bridge);
|
||||
const trustedSourceLinkedOfficialInstall = isTrustedSourceLinkedOfficialBridgeNpmInstall({
|
||||
targetPluginId,
|
||||
npmSpec,
|
||||
});
|
||||
let installSource = preferredSource;
|
||||
let installSpec = preferredSource === "clawhub" ? clawhubSpec : npmSpec;
|
||||
let result:
|
||||
@@ -1458,6 +1477,7 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
spec: npmSpec,
|
||||
mode: "update",
|
||||
expectedPluginId: targetPluginId,
|
||||
trustedSourceLinkedOfficialInstall,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
@@ -1466,6 +1486,7 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
spec: npmSpec,
|
||||
mode: "update",
|
||||
expectedPluginId: targetPluginId,
|
||||
trustedSourceLinkedOfficialInstall,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user