mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
Check ClawHub trust before plugin installs (#81307)
Merged via squash.
Prepared head SHA: 273fd7c20e
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Reviewed-by: @jesse-merhi
This commit is contained in:
@@ -134,6 +134,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Codex app-server: mirror native Codex subagent spawn lifecycle events into Task Registry so app-server child agents appear in task/status surfaces without relying on transcript text. (#79512) Thanks @mbelinky.
|
||||
- Gateway: expose optional `isHeartbeat` metadata on agent event payloads so clients can distinguish scheduled heartbeat runs from ordinary chat runs. (#80610) Thanks @medns.
|
||||
- Agents: add `agents.defaults.runRetries` and `agents.list[].runRetries` config for embedded Pi runner retry loop limits. (#80661) Thanks @medns.
|
||||
- Plugins/ClawHub: check exact-release trust before ClawHub plugin install/update downloads, require confirmation for risky releases, and add `--acknowledge-clawhub-risk` for reviewed automation. (#81307) Thanks @jesse-merhi.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ openclaw plugins install git:github.com/<owner>/<repo> # git repo
|
||||
openclaw plugins install git:github.com/<owner>/<repo>@<ref>
|
||||
openclaw plugins install <package> --force # overwrite existing install
|
||||
openclaw plugins install <package> --pin # pin version
|
||||
openclaw plugins install <package> --acknowledge-clawhub-risk
|
||||
openclaw plugins install <package> --dangerously-force-unsafe-install
|
||||
openclaw plugins install <path> # local path
|
||||
openclaw plugins install <plugin>@<marketplace> # marketplace
|
||||
@@ -135,6 +136,12 @@ is available, then fall back to `latest`.
|
||||
|
||||
If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/clawhub/security).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="--acknowledge-clawhub-risk">
|
||||
ClawHub installs check the selected release trust record before downloading the package. If ClawHub reports a risky scan status, risky moderation state, download block, or registry reason, OpenClaw shows the trust details and asks for confirmation before continuing.
|
||||
|
||||
Use `--acknowledge-clawhub-risk` only after reviewing the ClawHub warning and deciding to continue without an interactive prompt. Pending or stale clean trust records warn but do not require acknowledgement.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Hook packs and npm specs">
|
||||
`plugins install` is also the install surface for hook packs that expose `openclaw.hooks` in `package.json`. Use `openclaw hooks` for filtered hook visibility and per-hook enablement, not package installation.
|
||||
@@ -324,6 +331,7 @@ openclaw plugins update <id-or-npm-spec>
|
||||
openclaw plugins update --all
|
||||
openclaw plugins update <id-or-npm-spec> --dry-run
|
||||
openclaw plugins update @openclaw/voice-call
|
||||
openclaw plugins update openclaw-codex-app-server --acknowledge-clawhub-risk
|
||||
openclaw plugins update openclaw-codex-app-server --dangerously-force-unsafe-install
|
||||
```
|
||||
|
||||
@@ -351,6 +359,9 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked
|
||||
<Accordion title="--dangerously-force-unsafe-install on update">
|
||||
`--dangerously-force-unsafe-install` is also available on `plugins update` as a break-glass override for built-in dangerous-code scan false positives during plugin updates. It still does not bypass plugin `before_install` policy blocks or scan-failure blocking, and it only applies to plugin updates, not hook-pack updates.
|
||||
</Accordion>
|
||||
<Accordion title="--acknowledge-clawhub-risk on update">
|
||||
ClawHub-backed plugin updates run the same exact-release trust check as installs before downloading the replacement package. Use `--acknowledge-clawhub-risk` for reviewed automation that should continue when the selected ClawHub release has a risky trust warning.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Inspect
|
||||
|
||||
@@ -27,6 +27,7 @@ openclaw update --tag main
|
||||
openclaw update --dry-run
|
||||
openclaw update --no-restart
|
||||
openclaw update --yes
|
||||
openclaw update --acknowledge-clawhub-risk
|
||||
openclaw update --json
|
||||
openclaw --update
|
||||
```
|
||||
@@ -44,6 +45,10 @@ openclaw --update
|
||||
when npm plugin artifact drift is detected during post-update plugin sync.
|
||||
- `--timeout <seconds>`: per-step timeout (default is 1800s).
|
||||
- `--yes`: skip confirmation prompts (for example downgrade confirmation).
|
||||
- `--acknowledge-clawhub-risk`: continue post-update ClawHub plugin sync when
|
||||
the selected plugin release has a ClawHub trust warning. Without this flag,
|
||||
interactive runs ask before downloading risky ClawHub plugin releases and
|
||||
non-interactive runs fail closed.
|
||||
|
||||
`openclaw update` does not have a `--verbose` flag. Use `--dry-run` to preview
|
||||
the planned channel/tag/install/restart actions, `--json` for machine-readable
|
||||
|
||||
@@ -543,8 +543,10 @@ openclaw plugins install -l <path> # link (no copy) for dev
|
||||
openclaw plugins install <plugin> --marketplace <source>
|
||||
openclaw plugins install <plugin> --marketplace https://github.com/<owner>/<repo>
|
||||
openclaw plugins install <spec> --pin # record exact resolved npm spec
|
||||
openclaw plugins install <spec> --acknowledge-clawhub-risk
|
||||
openclaw plugins install <spec> --dangerously-force-unsafe-install
|
||||
openclaw plugins update <id-or-npm-spec> # update one plugin
|
||||
openclaw plugins update <id-or-npm-spec> --acknowledge-clawhub-risk
|
||||
openclaw plugins update <id-or-npm-spec> --dangerously-force-unsafe-install
|
||||
openclaw plugins update --all # update all
|
||||
openclaw plugins uninstall <id> # remove config and plugin index records
|
||||
@@ -603,6 +605,14 @@ beta release exists. Exact versions and explicit tags stay pinned.
|
||||
`--pin` is npm-only. It is not supported with `--marketplace`, because
|
||||
marketplace installs persist marketplace source metadata instead of an npm spec.
|
||||
|
||||
ClawHub-backed plugin installs and updates check the exact target release's
|
||||
ClawHub trust record before download. If ClawHub reports risk for that release,
|
||||
OpenClaw prints the package, version, scan status, moderation state, and reason
|
||||
codes, then requires confirmation before continuing. Non-interactive automation
|
||||
must pass `--acknowledge-clawhub-risk` after reviewing that warning. The flag
|
||||
acknowledges ClawHub registry risk only; it does not bypass plugin
|
||||
`before_install` hook policy blocks or the built-in dangerous-code scanner.
|
||||
|
||||
`--dangerously-force-unsafe-install` is a break-glass override for false
|
||||
positives from the built-in dangerous-code scanner. It allows plugin installs
|
||||
and plugin updates to continue past built-in `critical` findings, but it still
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ClawHubRiskAcknowledgementRequest } from "../plugins/clawhub.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
|
||||
export type ClawHubRiskAcknowledgementCliOptions = {
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
};
|
||||
|
||||
function canPromptForClawHubRisk(): boolean {
|
||||
return process.stdin.isTTY && process.stdout.isTTY;
|
||||
}
|
||||
|
||||
export function resolveClawHubRiskAcknowledgementCliOptions(params: {
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
action: "installing" | "updating";
|
||||
}): ClawHubRiskAcknowledgementCliOptions & {
|
||||
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => Promise<boolean>;
|
||||
} {
|
||||
return {
|
||||
acknowledgeClawHubRisk: params.acknowledgeClawHubRisk,
|
||||
onClawHubRisk:
|
||||
params.acknowledgeClawHubRisk || !canPromptForClawHubRisk()
|
||||
? undefined
|
||||
: async (request) =>
|
||||
await promptYesNo(
|
||||
`Continue ${params.action} ClawHub package "${request.packageName}@${request.version}" despite this warning?`,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -620,6 +620,33 @@ describe("plugins cli install", () => {
|
||||
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes ClawHub risk acknowledgement to explicit ClawHub installs", async () => {
|
||||
loadConfig.mockReturnValue(createEmptyPluginConfig());
|
||||
parseClawHubPluginSpec.mockReturnValue({ name: "demo" });
|
||||
installPluginFromClawHub.mockResolvedValue(
|
||||
createClawHubInstallResult({
|
||||
pluginId: "demo",
|
||||
packageName: "demo",
|
||||
version: "1.2.3",
|
||||
channel: "official",
|
||||
}),
|
||||
);
|
||||
enablePluginInConfig.mockReturnValue({ config: createEnabledPluginConfig("demo") });
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: createEnabledPluginConfig("demo"),
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "install", "clawhub:demo", "--acknowledge-clawhub-risk"]);
|
||||
|
||||
expect(installPluginFromClawHub).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:demo",
|
||||
acknowledgeClawHubRisk: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes the active profile extensions dir to ClawHub installs", async () => {
|
||||
const extensionsDir = useProfileExtensionsDir();
|
||||
const cfg = createEmptyPluginConfig();
|
||||
|
||||
+34
-3
@@ -18,10 +18,19 @@ import { applyParentDefaultHelpAction } from "./program/parent-default-help.js";
|
||||
|
||||
export type PluginUpdateOptions = {
|
||||
all?: boolean;
|
||||
acknowledgeClawhubRisk?: boolean;
|
||||
dryRun?: boolean;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
};
|
||||
|
||||
type CommanderClawHubRiskOptions = Record<string, unknown> & {
|
||||
acknowledgeClawhubRisk?: boolean;
|
||||
};
|
||||
|
||||
function normalizeCommanderClawHubRiskOption(opts: CommanderClawHubRiskOptions): boolean {
|
||||
return opts.acknowledgeClawhubRisk === true || opts.acknowledgeClawHubRisk === true;
|
||||
}
|
||||
|
||||
export type PluginMarketplaceListOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
@@ -253,6 +262,11 @@ export function registerPluginsCli(program: Command) {
|
||||
"Bypass built-in dangerous-code install blocking (plugin hooks may still block)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--acknowledge-clawhub-risk",
|
||||
"Acknowledge ClawHub release trust warnings without prompting",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--marketplace <source>",
|
||||
"Install a Claude marketplace plugin from a local repo/path or git/GitHub source",
|
||||
@@ -260,7 +274,7 @@ export function registerPluginsCli(program: Command) {
|
||||
.action(
|
||||
async (
|
||||
raw: string,
|
||||
opts: {
|
||||
opts: CommanderClawHubRiskOptions & {
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
force?: boolean;
|
||||
link?: boolean;
|
||||
@@ -272,7 +286,13 @@ export function registerPluginsCli(program: Command) {
|
||||
"install command",
|
||||
async () => {
|
||||
const { runPluginInstallCommand } = await import("./plugins-install-command.js");
|
||||
await runPluginInstallCommand({ raw, opts });
|
||||
await runPluginInstallCommand({
|
||||
raw,
|
||||
opts: {
|
||||
...opts,
|
||||
acknowledgeClawHubRisk: normalizeCommanderClawHubRiskOption(opts),
|
||||
},
|
||||
});
|
||||
},
|
||||
{ command: "install" },
|
||||
);
|
||||
@@ -290,9 +310,20 @@ export function registerPluginsCli(program: Command) {
|
||||
"Bypass built-in dangerous-code update blocking for plugins (plugin hooks may still block)",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--acknowledge-clawhub-risk",
|
||||
"Acknowledge ClawHub release trust warnings without prompting",
|
||||
false,
|
||||
)
|
||||
.action(async (id: string | undefined, opts: PluginUpdateOptions) => {
|
||||
const { runPluginUpdateCommand } = await import("./plugins-update-command.js");
|
||||
await runPluginUpdateCommand({ id, opts });
|
||||
await runPluginUpdateCommand({
|
||||
id,
|
||||
opts: {
|
||||
...opts,
|
||||
acknowledgeClawHubRisk: normalizeCommanderClawHubRiskOption(opts),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
plugins
|
||||
|
||||
@@ -210,6 +210,35 @@ describe("plugins cli update", () => {
|
||||
expect(updateParams.dangerouslyForceUnsafeInstall).toBe(true);
|
||||
});
|
||||
|
||||
it("passes ClawHub risk acknowledgement to plugin updates", async () => {
|
||||
const config = createTrackedPluginConfig({
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
spec: "openclaw-codex-app-server@beta",
|
||||
});
|
||||
loadConfig.mockReturnValue(config);
|
||||
setInstalledPluginIndexInstallRecords(config.plugins?.installs ?? {});
|
||||
updateNpmInstalledPlugins.mockResolvedValue({
|
||||
config,
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand([
|
||||
"plugins",
|
||||
"update",
|
||||
"openclaw-codex-app-server",
|
||||
"--acknowledge-clawhub-risk",
|
||||
]);
|
||||
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config,
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
acknowledgeClawHubRisk: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("writes updated config when updater reports changes", async () => {
|
||||
const cfg = {
|
||||
plugins: {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
import { resolveClawHubRiskAcknowledgementCliOptions } from "./clawhub-risk-acknowledgement.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { looksLikeLocalInstallSpec } from "./install-spec.js";
|
||||
import { resolvePinnedNpmInstallRecordForCli } from "./npm-resolution.js";
|
||||
@@ -558,6 +559,7 @@ export async function loadConfigForInstall(
|
||||
export async function runPluginInstallCommand(params: {
|
||||
raw: string;
|
||||
opts: InstallSafetyOverrides & {
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
force?: boolean;
|
||||
link?: boolean;
|
||||
pin?: boolean;
|
||||
@@ -941,6 +943,10 @@ export async function runPluginInstallCommand(params: {
|
||||
if (clawhubSpec) {
|
||||
const result = await installPluginFromClawHub({
|
||||
...safetyOverrides,
|
||||
...resolveClawHubRiskAcknowledgementCliOptions({
|
||||
acknowledgeClawHubRisk: opts.acknowledgeClawHubRisk,
|
||||
action: "installing",
|
||||
}),
|
||||
mode: installMode,
|
||||
spec: raw,
|
||||
extensionsDir,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { updateNpmInstalledPlugins } from "../plugins/update.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { resolveClawHubRiskAcknowledgementCliOptions } from "./clawhub-risk-acknowledgement.js";
|
||||
import { commitPluginInstallRecordsWithConfig } from "./plugins-install-record-commit.js";
|
||||
import { refreshPluginRegistryAfterConfigMutation } from "./plugins-registry-refresh.js";
|
||||
import { logPluginUpdateOutcomes } from "./plugins-update-outcomes.js";
|
||||
@@ -24,7 +25,12 @@ import { promptYesNo } from "./prompt.js";
|
||||
|
||||
export async function runPluginUpdateCommand(params: {
|
||||
id?: string;
|
||||
opts: { all?: boolean; dryRun?: boolean; dangerouslyForceUnsafeInstall?: boolean };
|
||||
opts: {
|
||||
all?: boolean;
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
dryRun?: boolean;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
};
|
||||
}) {
|
||||
assertConfigWriteAllowedInCurrentMode();
|
||||
|
||||
@@ -62,6 +68,10 @@ export async function runPluginUpdateCommand(params: {
|
||||
specOverrides: pluginSelection.specOverrides,
|
||||
dryRun: params.opts.dryRun,
|
||||
dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall,
|
||||
...resolveClawHubRiskAcknowledgementCliOptions({
|
||||
acknowledgeClawHubRisk: params.opts.acknowledgeClawHubRisk,
|
||||
action: "updating",
|
||||
}),
|
||||
logger,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
const specLabel = drift.resolvedSpec ?? drift.spec;
|
||||
|
||||
@@ -405,14 +405,14 @@ describe("update-cli", () => {
|
||||
|
||||
const syncPluginCall = (index = 0) => {
|
||||
const calls = syncPluginsForUpdateChannel.mock.calls as unknown as Array<
|
||||
[{ channel?: string; config?: OpenClawConfig }]
|
||||
[Record<string, unknown> & { channel?: string; config?: OpenClawConfig }]
|
||||
>;
|
||||
return calls[index]?.[0];
|
||||
};
|
||||
|
||||
const npmPluginUpdateCall = (index = 0) => {
|
||||
const calls = updateNpmInstalledPlugins.mock.calls as unknown as Array<
|
||||
[{ config?: OpenClawConfig; timeoutMs?: number }]
|
||||
[Record<string, unknown> & { config?: OpenClawConfig; timeoutMs?: number }]
|
||||
>;
|
||||
return calls[index]?.[0];
|
||||
};
|
||||
@@ -930,6 +930,33 @@ describe("update-cli", () => {
|
||||
expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("carries ClawHub risk acknowledgement into post-core resume", async () => {
|
||||
const { entrypoints } = setupUpdatedRootRefresh({
|
||||
gatewayUpdateImpl: async (root) =>
|
||||
makeOkUpdateResult({
|
||||
mode: "git",
|
||||
root,
|
||||
before: { sha: "old-sha", version: "2026.4.26" },
|
||||
after: { sha: "new-sha", version: "2026.4.27" },
|
||||
}),
|
||||
});
|
||||
|
||||
await updateCommand({
|
||||
channel: "dev",
|
||||
yes: true,
|
||||
restart: false,
|
||||
acknowledgeClawHubRisk: true,
|
||||
});
|
||||
|
||||
expect(spawnCall()?.[1]).toEqual([
|
||||
entrypoints[0],
|
||||
"update",
|
||||
"--no-restart",
|
||||
"--yes",
|
||||
"--acknowledge-clawhub-risk",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps downgrade post-update work in the current process", async () => {
|
||||
const downgradedRoot = createCaseDir("openclaw-downgraded-root");
|
||||
setupUpdatedRootRefresh({
|
||||
@@ -2892,6 +2919,22 @@ describe("update-cli", () => {
|
||||
expect(updateCall?.syncOfficialPluginInstalls).toBe(true);
|
||||
});
|
||||
|
||||
it("forwards ClawHub risk acknowledgement to post-update plugin work", async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
mockPackageInstallStatus(tempDir);
|
||||
|
||||
await updateCommand({
|
||||
channel: "beta",
|
||||
yes: true,
|
||||
restart: false,
|
||||
acknowledgeClawHubRisk: true,
|
||||
});
|
||||
|
||||
expect(syncPluginCall()?.acknowledgeClawHubRisk).toBe(true);
|
||||
expect(npmPluginUpdateCall()?.acknowledgeClawHubRisk).toBe(true);
|
||||
expect(lastNpmPluginUpdateCall()?.acknowledgeClawHubRisk).toBe(true);
|
||||
});
|
||||
|
||||
it("persists channel and runs post-update work after switching from package to git", async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
const gitRoot = path.join(tempDir, "..", "openclaw");
|
||||
|
||||
+27
-4
@@ -31,6 +31,21 @@ function inheritedUpdateTimeout(
|
||||
return inheritOptionFromParent<string>(command, "timeout");
|
||||
}
|
||||
|
||||
type CommanderUpdateOptions = Record<string, unknown> & {
|
||||
acknowledgeClawhubRisk?: boolean;
|
||||
channel?: string;
|
||||
dryRun?: boolean;
|
||||
json?: boolean;
|
||||
restart?: boolean;
|
||||
tag?: string;
|
||||
timeout?: string;
|
||||
yes?: boolean;
|
||||
};
|
||||
|
||||
function normalizeCommanderClawHubRiskOption(opts: CommanderUpdateOptions): boolean {
|
||||
return opts.acknowledgeClawhubRisk === true || opts.acknowledgeClawHubRisk === true;
|
||||
}
|
||||
|
||||
export function registerUpdateCli(program: Command) {
|
||||
program.enablePositionalOptions();
|
||||
const update = program
|
||||
@@ -46,6 +61,11 @@ export function registerUpdateCli(program: Command) {
|
||||
)
|
||||
.option("--timeout <seconds>", "Timeout for each update step in seconds (default: 1800)")
|
||||
.option("--yes", "Skip confirmation prompts (non-interactive)", false)
|
||||
.option(
|
||||
"--acknowledge-clawhub-risk",
|
||||
"Acknowledge ClawHub release trust warnings during post-update plugin sync",
|
||||
false,
|
||||
)
|
||||
.addHelpText("after", () => {
|
||||
const examples = [
|
||||
["openclaw update", "Update a source checkout (git)"],
|
||||
@@ -57,6 +77,7 @@ export function registerUpdateCli(program: Command) {
|
||||
["openclaw update --no-restart", "Update without restarting the service"],
|
||||
["openclaw update --json", "Output result as JSON"],
|
||||
["openclaw update --yes", "Non-interactive (accept downgrade prompts)"],
|
||||
["openclaw update --acknowledge-clawhub-risk", "Acknowledge ClawHub plugin trust warnings"],
|
||||
["openclaw update wizard", "Interactive update wizard"],
|
||||
["openclaw --update", "Shorthand for openclaw update"],
|
||||
] as const;
|
||||
@@ -75,6 +96,7 @@ ${theme.heading("Switch channels:")}
|
||||
|
||||
${theme.heading("Non-interactive:")}
|
||||
- Use --yes to accept downgrade prompts
|
||||
- Use --acknowledge-clawhub-risk only after reviewing ClawHub plugin trust warnings
|
||||
- Combine with --channel/--tag/--no-restart/--json/--timeout as needed
|
||||
- Use --dry-run to preview actions without writing config/installing/restarting
|
||||
|
||||
@@ -89,16 +111,17 @@ ${theme.heading("Notes:")}
|
||||
|
||||
${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.openclaw.ai/cli/update")}`;
|
||||
})
|
||||
.action(async (opts) => {
|
||||
.action(async (opts: CommanderUpdateOptions) => {
|
||||
try {
|
||||
await updateCommand({
|
||||
json: Boolean(opts.json),
|
||||
restart: Boolean(opts.restart),
|
||||
dryRun: Boolean(opts.dryRun),
|
||||
channel: opts.channel as string | undefined,
|
||||
tag: opts.tag as string | undefined,
|
||||
timeout: opts.timeout as string | undefined,
|
||||
channel: opts.channel,
|
||||
tag: opts.tag,
|
||||
timeout: opts.timeout,
|
||||
yes: Boolean(opts.yes),
|
||||
acknowledgeClawHubRisk: normalizeCommanderClawHubRiskOption(opts),
|
||||
});
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
|
||||
@@ -32,6 +32,7 @@ export type UpdateCommandOptions = {
|
||||
tag?: string;
|
||||
timeout?: string;
|
||||
yes?: boolean;
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
};
|
||||
|
||||
export type UpdateStatusOptions = {
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
resolvePnpmGlobalDirFromGlobalRoot,
|
||||
} from "../../infra/update-global.js";
|
||||
import { runGatewayUpdate, type UpdateRunResult } from "../../infra/update-runner.js";
|
||||
import type { ClawHubRiskAcknowledgementRequest } from "../../plugins/clawhub.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../../plugins/config-state.js";
|
||||
import {
|
||||
loadInstalledPluginIndexInstallRecords,
|
||||
@@ -173,6 +174,29 @@ type MissingPluginInstallPayload = {
|
||||
|
||||
type PostUpdatePluginWarning = NonNullable<PostCorePluginUpdateResult["warnings"]>[number];
|
||||
|
||||
function resolveUpdateClawHubRiskAcknowledgementOptions(opts: UpdateCommandOptions): {
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => Promise<boolean>;
|
||||
} {
|
||||
if (opts.acknowledgeClawHubRisk) {
|
||||
return { acknowledgeClawHubRisk: true };
|
||||
}
|
||||
if (opts.json || !process.stdin.isTTY) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
onClawHubRisk: async (request) => {
|
||||
const ok = await confirm({
|
||||
message: stylePromptMessage(
|
||||
`Continue updating ClawHub package "${request.packageName}@${request.version}" despite this warning?`,
|
||||
),
|
||||
initialValue: false,
|
||||
});
|
||||
return !isCancel(ok) && ok;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function pickUpdateQuip(): string {
|
||||
return UPDATE_QUIPS[Math.floor(Math.random() * UPDATE_QUIPS.length)] ?? "Update complete.";
|
||||
}
|
||||
@@ -1185,6 +1209,9 @@ export async function updatePluginsAfterCoreUpdate(params: {
|
||||
}
|
||||
|
||||
const warnings: PostUpdatePluginWarning[] = [];
|
||||
const clawHubRiskAcknowledgementOptions = resolveUpdateClawHubRiskAcknowledgementOptions(
|
||||
params.opts,
|
||||
);
|
||||
const pluginInstallRecords =
|
||||
params.pluginInstallRecords ?? (await loadInstalledPluginIndexInstallRecords());
|
||||
const syncConfig = withPluginInstallRecords(
|
||||
@@ -1198,6 +1225,7 @@ export async function updatePluginsAfterCoreUpdate(params: {
|
||||
externalizedBundledPluginBridges: await listPersistedBundledPluginLocationBridges({
|
||||
workspaceDir: params.root,
|
||||
}),
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
logger: pluginLogger,
|
||||
});
|
||||
for (const error of syncResult.summary.errors) {
|
||||
@@ -1271,6 +1299,7 @@ export async function updatePluginsAfterCoreUpdate(params: {
|
||||
disableOnFailure: true,
|
||||
logger: pluginLogger,
|
||||
onIntegrityDrift: onPluginIntegrityDrift,
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
});
|
||||
pluginConfig = repairResult.config;
|
||||
pluginsChanged ||= repairResult.changed;
|
||||
@@ -1291,6 +1320,7 @@ export async function updatePluginsAfterCoreUpdate(params: {
|
||||
disableOnFailure: true,
|
||||
logger: pluginLogger,
|
||||
onIntegrityDrift: onPluginIntegrityDrift,
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
});
|
||||
pluginConfig = npmResult.config;
|
||||
pluginsChanged ||= npmResult.changed;
|
||||
@@ -1949,6 +1979,9 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
if (params.opts.yes) {
|
||||
argv.push("--yes");
|
||||
}
|
||||
if (params.opts.acknowledgeClawHubRisk) {
|
||||
argv.push("--acknowledge-clawhub-risk");
|
||||
}
|
||||
if (params.opts.timeout) {
|
||||
argv.push("--timeout", params.opts.timeout);
|
||||
}
|
||||
|
||||
@@ -506,6 +506,7 @@ async function installCandidate(params: {
|
||||
const { candidate } = params;
|
||||
const extensionsDir = resolveDefaultPluginExtensionsDir();
|
||||
const changes: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const clawhubSpecs = candidate.clawhubSpec
|
||||
? resolveClawHubInstallSpecsForUpdateChannel({
|
||||
spec: candidate.clawhubSpec,
|
||||
@@ -526,6 +527,9 @@ async function installCandidate(params: {
|
||||
extensionsDir,
|
||||
expectedPluginId: candidate.pluginId,
|
||||
mode: "install",
|
||||
logger: {
|
||||
warn: (message) => warnings.push(message),
|
||||
},
|
||||
});
|
||||
if (clawhubResult.ok) {
|
||||
const pluginId = clawhubResult.pluginId;
|
||||
@@ -540,7 +544,7 @@ async function installCandidate(params: {
|
||||
},
|
||||
},
|
||||
changes: [`Installed missing configured plugin "${pluginId}" from ${clawhubInstallSpec}.`],
|
||||
warnings: [],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
if (!npmInstallSpec || !shouldFallbackClawHubToNpm(clawhubResult)) {
|
||||
@@ -548,6 +552,7 @@ async function installCandidate(params: {
|
||||
records: params.records,
|
||||
changes: [],
|
||||
warnings: [
|
||||
...warnings,
|
||||
`Failed to install missing configured plugin "${candidate.pluginId}" from ${clawhubInstallSpec}: ${clawhubResult.error}`,
|
||||
],
|
||||
};
|
||||
@@ -561,6 +566,7 @@ async function installCandidate(params: {
|
||||
records: params.records,
|
||||
changes: [],
|
||||
warnings: [
|
||||
...warnings,
|
||||
`Failed to install missing configured plugin "${candidate.pluginId}": missing npm spec.`,
|
||||
],
|
||||
};
|
||||
@@ -580,6 +586,7 @@ async function installCandidate(params: {
|
||||
records: params.records,
|
||||
changes: [],
|
||||
warnings: [
|
||||
...warnings,
|
||||
`Failed to install missing configured plugin "${candidate.pluginId}" from ${npmInstallSpec}: ${result.error}`,
|
||||
],
|
||||
};
|
||||
@@ -601,7 +608,7 @@ async function installCandidate(params: {
|
||||
...changes,
|
||||
`Installed missing configured plugin "${pluginId}" from ${npmInstallSpec}.`,
|
||||
],
|
||||
warnings: [],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ type NpmSpecInstallCall = {
|
||||
type ClawHubInstallCall = {
|
||||
expectedPluginId?: string;
|
||||
mode?: string;
|
||||
onClawHubRisk?: unknown;
|
||||
spec?: string;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
@@ -368,6 +369,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
expect(clawHubCall.expectedPluginId).toBe("demo-plugin");
|
||||
expect(clawHubCall.mode).toBe("install");
|
||||
expect(clawHubCall.timeoutMs).toBe(300_000);
|
||||
expect(typeof clawHubCall.onClawHubRisk).toBe("function");
|
||||
expect(update).toHaveBeenCalledWith("Downloading");
|
||||
expect(stop).toHaveBeenCalledWith("Installed Demo Provider plugin");
|
||||
const [, recordUpdate] = readFirstMockCall(recordPluginInstall, "recordPluginInstall") as [
|
||||
|
||||
@@ -889,6 +889,14 @@ async function installPluginFromClawHubSpecWithProgress(params: {
|
||||
logInstallWarningWithSpacing(params.runtime, message);
|
||||
},
|
||||
},
|
||||
onClawHubRisk: async (request) => {
|
||||
animated.stop();
|
||||
progress.stop("Review ClawHub warning");
|
||||
return await params.prompter.confirm({
|
||||
message: `Continue installing ClawHub package "${sanitizeTerminalText(request.packageName)}@${sanitizeTerminalText(request.version)}" despite this warning?`,
|
||||
initialValue: false,
|
||||
});
|
||||
},
|
||||
}),
|
||||
ONBOARDING_PLUGIN_INSTALL_WATCHDOG_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
+57
-12
@@ -330,30 +330,75 @@ describe("clawhub helpers", () => {
|
||||
requestedUrl = input instanceof Request ? input.url : String(input);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
releaseId: "rel_demo",
|
||||
state: "approved",
|
||||
reasonCode: "clean",
|
||||
createdAt: 1774256733107,
|
||||
scanState: "clean",
|
||||
moderationState: "approved",
|
||||
package: {
|
||||
name: "@openclaw/diagnostics-otel",
|
||||
displayName: "Diagnostics",
|
||||
family: "code-plugin",
|
||||
},
|
||||
release: {
|
||||
id: "rel_demo",
|
||||
version: "2026.3.22",
|
||||
},
|
||||
trust: {
|
||||
scanStatus: "clean",
|
||||
moderationState: null,
|
||||
blockedFromDownload: false,
|
||||
reasons: [],
|
||||
pending: false,
|
||||
stale: true,
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
releaseId: "rel_demo",
|
||||
state: "approved",
|
||||
reasonCode: "clean",
|
||||
createdAt: 1774256733107,
|
||||
scanState: "clean",
|
||||
moderationState: "approved",
|
||||
package: {
|
||||
name: "@openclaw/diagnostics-otel",
|
||||
displayName: "Diagnostics",
|
||||
family: "code-plugin",
|
||||
},
|
||||
release: {
|
||||
id: "rel_demo",
|
||||
version: "2026.3.22",
|
||||
},
|
||||
trust: {
|
||||
scanStatus: "clean",
|
||||
moderationState: null,
|
||||
blockedFromDownload: false,
|
||||
reasons: [],
|
||||
pending: false,
|
||||
stale: true,
|
||||
},
|
||||
});
|
||||
expect(new URL(requestedUrl).pathname).toBe(
|
||||
"/api/v1/packages/%40openclaw%2Fdiagnostics-otel/versions/2026.3.22/security",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects malformed package security reports", async () => {
|
||||
await expect(
|
||||
fetchClawHubPackageSecurity({
|
||||
name: "@openclaw/diagnostics-otel",
|
||||
version: "2026.3.22",
|
||||
fetchImpl: async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
trust: {
|
||||
scanStatus: "clean",
|
||||
moderationState: null,
|
||||
blockedFromDownload: false,
|
||||
reasons: "clean",
|
||||
pending: false,
|
||||
stale: false,
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
}),
|
||||
).rejects.toThrow("expected reasons to be a string array");
|
||||
});
|
||||
|
||||
it("downloads package archives to sanitized temp paths and cleans them up", async () => {
|
||||
const archive = await downloadClawHubPackageArchive({
|
||||
name: "@hyf/zai-external-alpha",
|
||||
|
||||
+140
-18
@@ -62,14 +62,14 @@ export type ClawHubArtifactScanState =
|
||||
| "not-run"
|
||||
| (string & {});
|
||||
export type ClawHubArtifactModerationState = "approved" | "quarantined" | "revoked" | (string & {});
|
||||
export type ClawHubPackageSecurityState =
|
||||
| "pending"
|
||||
| "approved"
|
||||
| "limited"
|
||||
| "quarantined"
|
||||
| "rejected"
|
||||
| "revoked"
|
||||
| (string & {});
|
||||
export type ClawHubPackageSecurityTrust = {
|
||||
scanStatus?: ClawHubArtifactScanState | null;
|
||||
moderationState?: ClawHubArtifactModerationState | null;
|
||||
blockedFromDownload: boolean;
|
||||
reasons: string[];
|
||||
pending: boolean;
|
||||
stale: boolean;
|
||||
};
|
||||
export type ClawHubResolvedArtifact =
|
||||
| {
|
||||
source: "clawhub";
|
||||
@@ -116,15 +116,16 @@ export type ClawHubPackageArtifactResolverResponse = {
|
||||
artifact?: ClawHubResolvedArtifact | null;
|
||||
};
|
||||
export type ClawHubPackageSecurityResponse = {
|
||||
packageId?: string | null;
|
||||
releaseId?: string | null;
|
||||
state: ClawHubPackageSecurityState;
|
||||
reasonCode?: string | null;
|
||||
moderatorNote?: string | null;
|
||||
actorId?: string | null;
|
||||
createdAt?: number | null;
|
||||
scanState?: ClawHubArtifactScanState | null;
|
||||
moderationState?: ClawHubArtifactModerationState | null;
|
||||
package?: {
|
||||
name?: string | null;
|
||||
displayName?: string | null;
|
||||
family?: ClawHubPackageFamily | (string & {}) | null;
|
||||
} | null;
|
||||
release?: {
|
||||
id?: string | null;
|
||||
version?: string | null;
|
||||
} | null;
|
||||
trust: ClawHubPackageSecurityTrust;
|
||||
};
|
||||
export type ClawHubPackageClawPackSummary = {
|
||||
available: boolean;
|
||||
@@ -636,6 +637,126 @@ async function fetchJson<T>(params: ClawHubRequestParams): Promise<T> {
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function optionalStringField(
|
||||
source: Record<string, unknown>,
|
||||
field: string,
|
||||
context: string,
|
||||
): string | null | undefined {
|
||||
const value = source[field];
|
||||
if (value === undefined || value === null || typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Malformed ClawHub ${context}: expected ${field} to be a string or null.`);
|
||||
}
|
||||
|
||||
function requiredBooleanField(
|
||||
source: Record<string, unknown>,
|
||||
field: string,
|
||||
context: string,
|
||||
): boolean {
|
||||
const value = source[field];
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Malformed ClawHub ${context}: expected ${field} to be a boolean.`);
|
||||
}
|
||||
|
||||
function requiredStringArrayField(
|
||||
source: Record<string, unknown>,
|
||||
field: string,
|
||||
context: string,
|
||||
): string[] {
|
||||
const value = source[field];
|
||||
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Malformed ClawHub ${context}: expected ${field} to be a string array.`);
|
||||
}
|
||||
|
||||
function parseOptionalSecurityPackage(value: unknown): ClawHubPackageSecurityResponse["package"] {
|
||||
if (value === undefined || value === null) {
|
||||
return value;
|
||||
}
|
||||
if (!isJsonObject(value)) {
|
||||
throw new Error(
|
||||
"Malformed ClawHub security response: expected package to be an object or null.",
|
||||
);
|
||||
}
|
||||
const result: NonNullable<ClawHubPackageSecurityResponse["package"]> = {};
|
||||
const name = optionalStringField(value, "name", "security package");
|
||||
const displayName = optionalStringField(value, "displayName", "security package");
|
||||
const family = optionalStringField(value, "family", "security package");
|
||||
if (name !== undefined) {
|
||||
result.name = name;
|
||||
}
|
||||
if (displayName !== undefined) {
|
||||
result.displayName = displayName;
|
||||
}
|
||||
if (family !== undefined) {
|
||||
result.family = family;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseOptionalSecurityRelease(value: unknown): ClawHubPackageSecurityResponse["release"] {
|
||||
if (value === undefined || value === null) {
|
||||
return value;
|
||||
}
|
||||
if (!isJsonObject(value)) {
|
||||
throw new Error(
|
||||
"Malformed ClawHub security response: expected release to be an object or null.",
|
||||
);
|
||||
}
|
||||
const result: NonNullable<ClawHubPackageSecurityResponse["release"]> = {};
|
||||
const id = optionalStringField(value, "id", "security release");
|
||||
const version = optionalStringField(value, "version", "security release");
|
||||
if (id !== undefined) {
|
||||
result.id = id;
|
||||
}
|
||||
if (version !== undefined) {
|
||||
result.version = version;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseClawHubPackageSecurityResponse(value: unknown): ClawHubPackageSecurityResponse {
|
||||
if (!isJsonObject(value)) {
|
||||
throw new Error("Malformed ClawHub security response: expected an object.");
|
||||
}
|
||||
const trust = value.trust;
|
||||
if (!isJsonObject(trust)) {
|
||||
throw new Error("Malformed ClawHub security response: expected trust to be an object.");
|
||||
}
|
||||
const parsedTrust: ClawHubPackageSecurityTrust = {
|
||||
blockedFromDownload: requiredBooleanField(trust, "blockedFromDownload", "security trust"),
|
||||
reasons: requiredStringArrayField(trust, "reasons", "security trust"),
|
||||
pending: requiredBooleanField(trust, "pending", "security trust"),
|
||||
stale: requiredBooleanField(trust, "stale", "security trust"),
|
||||
};
|
||||
const scanStatus = optionalStringField(trust, "scanStatus", "security trust");
|
||||
const moderationState = optionalStringField(trust, "moderationState", "security trust");
|
||||
if (scanStatus !== undefined) {
|
||||
parsedTrust.scanStatus = scanStatus;
|
||||
}
|
||||
if (moderationState !== undefined) {
|
||||
parsedTrust.moderationState = moderationState;
|
||||
}
|
||||
const result: ClawHubPackageSecurityResponse = { trust: parsedTrust };
|
||||
const parsedPackage = parseOptionalSecurityPackage(value.package);
|
||||
const parsedRelease = parseOptionalSecurityRelease(value.release);
|
||||
if (parsedPackage !== undefined) {
|
||||
result.package = parsedPackage;
|
||||
}
|
||||
if (parsedRelease !== undefined) {
|
||||
result.release = parsedRelease;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveClawHubBaseUrl(baseUrl?: string): string {
|
||||
return normalizeBaseUrl(baseUrl);
|
||||
}
|
||||
@@ -768,7 +889,7 @@ export async function fetchClawHubPackageSecurity(params: {
|
||||
timeoutMs?: number;
|
||||
fetchImpl?: FetchLike;
|
||||
}): Promise<ClawHubPackageSecurityResponse> {
|
||||
return await fetchJson<ClawHubPackageSecurityResponse>({
|
||||
const response = await fetchJson<unknown>({
|
||||
baseUrl: params.baseUrl,
|
||||
path: `/api/v1/packages/${encodeURIComponent(params.name)}/versions/${encodeURIComponent(
|
||||
params.version,
|
||||
@@ -777,6 +898,7 @@ export async function fetchClawHubPackageSecurity(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
fetchImpl: params.fetchImpl,
|
||||
});
|
||||
return parseClawHubPackageSecurityResponse(response);
|
||||
}
|
||||
|
||||
export async function fetchClawHubPackageReadiness(params: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const parseClawHubPluginSpecMock = vi.fn();
|
||||
const fetchClawHubPackageDetailMock = vi.fn();
|
||||
const fetchClawHubPackageArtifactMock = vi.fn();
|
||||
const fetchClawHubPackageSecurityMock = vi.fn();
|
||||
const fetchClawHubPackageVersionMock = vi.fn();
|
||||
const downloadClawHubPackageArchiveMock = vi.fn();
|
||||
const archiveCleanupMock = vi.fn();
|
||||
@@ -23,6 +24,7 @@ vi.mock("../infra/clawhub.js", async () => {
|
||||
parseClawHubPluginSpec: (...args: unknown[]) => parseClawHubPluginSpecMock(...args),
|
||||
fetchClawHubPackageDetail: (...args: unknown[]) => fetchClawHubPackageDetailMock(...args),
|
||||
fetchClawHubPackageArtifact: (...args: unknown[]) => fetchClawHubPackageArtifactMock(...args),
|
||||
fetchClawHubPackageSecurity: (...args: unknown[]) => fetchClawHubPackageSecurityMock(...args),
|
||||
fetchClawHubPackageVersion: (...args: unknown[]) => fetchClawHubPackageVersionMock(...args),
|
||||
downloadClawHubPackageArchive: (...args: unknown[]) =>
|
||||
downloadClawHubPackageArchiveMock(...args),
|
||||
@@ -142,6 +144,8 @@ function expectClawHubInstallFlow(params: {
|
||||
expect(packageVersionCall().version).toBe(params.version);
|
||||
expect(packageArtifactCall().name).toBe("demo");
|
||||
expect(packageArtifactCall().version).toBe(params.version);
|
||||
expect(packageSecurityCall().name).toBe("demo");
|
||||
expect(packageSecurityCall().version).toBe(params.version);
|
||||
expect(archiveInstallCall().archivePath).toBe(params.archivePath);
|
||||
}
|
||||
|
||||
@@ -211,6 +215,10 @@ function packageArtifactCall(callIndex = 0): PackageLookupCall {
|
||||
return mockCallArg(fetchClawHubPackageArtifactMock, callIndex) as PackageLookupCall;
|
||||
}
|
||||
|
||||
function packageSecurityCall(callIndex = 0): PackageLookupCall {
|
||||
return mockCallArg(fetchClawHubPackageSecurityMock, callIndex) as PackageLookupCall;
|
||||
}
|
||||
|
||||
function archiveDownloadCall(callIndex = 0): PackageLookupCall {
|
||||
return mockCallArg(downloadClawHubPackageArchiveMock, callIndex) as PackageLookupCall;
|
||||
}
|
||||
@@ -250,6 +258,7 @@ describe("installPluginFromClawHub", () => {
|
||||
parseClawHubPluginSpecMock.mockReset();
|
||||
fetchClawHubPackageDetailMock.mockReset();
|
||||
fetchClawHubPackageArtifactMock.mockReset();
|
||||
fetchClawHubPackageSecurityMock.mockReset();
|
||||
fetchClawHubPackageVersionMock.mockReset();
|
||||
downloadClawHubPackageArchiveMock.mockReset();
|
||||
archiveCleanupMock.mockReset();
|
||||
@@ -289,6 +298,24 @@ describe("installPluginFromClawHub", () => {
|
||||
fetchClawHubPackageArtifactMock.mockImplementation((params) =>
|
||||
fetchClawHubPackageVersionMock(params),
|
||||
);
|
||||
fetchClawHubPackageSecurityMock.mockResolvedValue({
|
||||
package: {
|
||||
name: "demo",
|
||||
displayName: "Demo",
|
||||
family: "code-plugin",
|
||||
},
|
||||
release: {
|
||||
version: "2026.3.22",
|
||||
},
|
||||
trust: {
|
||||
scanStatus: "clean",
|
||||
moderationState: null,
|
||||
blockedFromDownload: false,
|
||||
reasons: [],
|
||||
pending: false,
|
||||
stale: false,
|
||||
},
|
||||
});
|
||||
downloadClawHubPackageArchiveMock.mockResolvedValue({
|
||||
archivePath: "/tmp/clawhub-demo/archive.zip",
|
||||
integrity: DEMO_ARCHIVE_INTEGRITY,
|
||||
@@ -331,6 +358,111 @@ describe("installPluginFromClawHub", () => {
|
||||
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("requires acknowledgement before downloading risky ClawHub releases", async () => {
|
||||
fetchClawHubPackageSecurityMock.mockResolvedValueOnce({
|
||||
trust: {
|
||||
scanStatus: "malicious",
|
||||
moderationState: "quarantined",
|
||||
blockedFromDownload: true,
|
||||
reasons: ["manual_moderation"],
|
||||
pending: false,
|
||||
stale: false,
|
||||
},
|
||||
});
|
||||
const logger = createLoggerSpies();
|
||||
|
||||
const result = await installPluginFromClawHub({
|
||||
spec: "clawhub:demo",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
logger,
|
||||
});
|
||||
|
||||
const failure = expectInstallFailure(result);
|
||||
expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_RISK_ACKNOWLEDGEMENT_REQUIRED);
|
||||
expect(failure.error).toContain("--acknowledge-clawhub-risk");
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('ClawHub trust warning for "demo@2026.3.22"'),
|
||||
);
|
||||
expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled();
|
||||
expect(installPluginFromArchiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues after a risky ClawHub release is acknowledged", async () => {
|
||||
const onClawHubRisk = vi.fn(async () => true);
|
||||
fetchClawHubPackageSecurityMock.mockResolvedValueOnce({
|
||||
trust: {
|
||||
scanStatus: "suspicious",
|
||||
moderationState: null,
|
||||
blockedFromDownload: false,
|
||||
reasons: ["payload_strings"],
|
||||
pending: false,
|
||||
stale: false,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await installPluginFromClawHub({
|
||||
spec: "clawhub:demo",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
onClawHubRisk,
|
||||
});
|
||||
|
||||
expectSuccessfulClawHubInstall(result);
|
||||
expect(onClawHubRisk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
packageName: "demo",
|
||||
version: "2026.3.22",
|
||||
warning: expect.stringContaining("payload_strings"),
|
||||
}),
|
||||
);
|
||||
expect(downloadClawHubPackageArchiveMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns for stale clean ClawHub trust without requiring acknowledgement", async () => {
|
||||
const onClawHubRisk = vi.fn(async () => false);
|
||||
const logger = createLoggerSpies();
|
||||
fetchClawHubPackageSecurityMock.mockResolvedValueOnce({
|
||||
trust: {
|
||||
scanStatus: "clean",
|
||||
moderationState: null,
|
||||
blockedFromDownload: false,
|
||||
reasons: [],
|
||||
pending: false,
|
||||
stale: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await installPluginFromClawHub({
|
||||
spec: "clawhub:demo",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
logger,
|
||||
onClawHubRisk,
|
||||
});
|
||||
|
||||
expectSuccessfulClawHubInstall(result);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("stale=true"));
|
||||
expect(onClawHubRisk).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops when the ClawHub security response is unavailable", async () => {
|
||||
fetchClawHubPackageSecurityMock.mockRejectedValueOnce(
|
||||
new ClawHubRequestError({
|
||||
path: "/api/v1/packages/demo/versions/2026.3.22/security",
|
||||
status: 404,
|
||||
body: "not found",
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await installPluginFromClawHub({
|
||||
spec: "clawhub:demo",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
const failure = expectInstallFailure(result);
|
||||
expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_SECURITY_UNAVAILABLE);
|
||||
expect(failure.error).toContain("ClawHub release trust check failed");
|
||||
expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks official source-linked OpenClaw packages as trusted for install scanning", async () => {
|
||||
fetchClawHubPackageDetailMock.mockResolvedValueOnce({
|
||||
package: {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
downloadClawHubPackageArchive,
|
||||
fetchClawHubPackageArtifact,
|
||||
fetchClawHubPackageDetail,
|
||||
fetchClawHubPackageSecurity,
|
||||
fetchClawHubPackageVersion,
|
||||
normalizeClawHubSha256Integrity,
|
||||
normalizeClawHubSha256Hex,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
type ClawHubPackageCompatibility,
|
||||
type ClawHubPackageDetail,
|
||||
type ClawHubPackageClawPackSummary,
|
||||
type ClawHubPackageSecurityTrust,
|
||||
type ClawHubResolvedArtifact,
|
||||
type ClawHubPackageVersion,
|
||||
} from "../infra/clawhub.js";
|
||||
@@ -49,6 +51,8 @@ export const CLAWHUB_INSTALL_ERROR_CODE = {
|
||||
INCOMPATIBLE_GATEWAY: "incompatible_gateway",
|
||||
MISSING_ARCHIVE_INTEGRITY: "missing_archive_integrity",
|
||||
ARCHIVE_INTEGRITY_MISMATCH: "archive_integrity_mismatch",
|
||||
CLAWHUB_SECURITY_UNAVAILABLE: "clawhub_security_unavailable",
|
||||
CLAWHUB_RISK_ACKNOWLEDGEMENT_REQUIRED: "clawhub_risk_acknowledgement_required",
|
||||
} as const;
|
||||
|
||||
export type ClawHubInstallErrorCode =
|
||||
@@ -59,6 +63,13 @@ type PluginInstallLogger = {
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type ClawHubRiskAcknowledgementRequest = {
|
||||
packageName: string;
|
||||
version: string;
|
||||
trust: ClawHubPackageSecurityTrust;
|
||||
warning: string;
|
||||
};
|
||||
|
||||
type ClawHubInstallFailure = {
|
||||
ok: false;
|
||||
error: string;
|
||||
@@ -349,6 +360,121 @@ function mapClawHubRequestError(
|
||||
return buildClawHubInstallFailure(formatErrorMessage(error));
|
||||
}
|
||||
|
||||
const CLAWHUB_RISK_SCAN_STATUSES = new Set(["malicious", "suspicious"]);
|
||||
const CLAWHUB_RISK_MODERATION_STATES = new Set(["blocked", "quarantined", "revoked"]);
|
||||
const CLAWHUB_NON_RISK_REASONS = new Set([
|
||||
"pending",
|
||||
"pending_scan",
|
||||
"scan_pending",
|
||||
"stale",
|
||||
"stale_scan",
|
||||
]);
|
||||
|
||||
function normalizeClawHubTrustToken(value: string | null | undefined): string {
|
||||
return normalizeOptionalString(value)?.toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
function resolveClawHubRiskReasons(trust: ClawHubPackageSecurityTrust): string[] {
|
||||
const reasons: string[] = [];
|
||||
if (trust.blockedFromDownload) {
|
||||
reasons.push("blocked from download");
|
||||
}
|
||||
const scanStatus = normalizeClawHubTrustToken(trust.scanStatus);
|
||||
if (CLAWHUB_RISK_SCAN_STATUSES.has(scanStatus)) {
|
||||
reasons.push(`scan status ${scanStatus}`);
|
||||
}
|
||||
const moderationState = normalizeClawHubTrustToken(trust.moderationState);
|
||||
if (CLAWHUB_RISK_MODERATION_STATES.has(moderationState)) {
|
||||
reasons.push(`moderation state ${moderationState}`);
|
||||
}
|
||||
for (const reason of trust.reasons) {
|
||||
const normalized = normalizeClawHubTrustToken(reason);
|
||||
if (normalized && !CLAWHUB_NON_RISK_REASONS.has(normalized)) {
|
||||
reasons.push(reason);
|
||||
}
|
||||
}
|
||||
return reasons;
|
||||
}
|
||||
|
||||
function formatClawHubTrustWarning(params: {
|
||||
packageName: string;
|
||||
version: string;
|
||||
trust: ClawHubPackageSecurityTrust;
|
||||
riskReasons: readonly string[];
|
||||
}): string {
|
||||
const details = [
|
||||
`scan=${params.trust.scanStatus ?? "unknown"}`,
|
||||
`moderation=${params.trust.moderationState ?? "none"}`,
|
||||
`blockedFromDownload=${String(params.trust.blockedFromDownload)}`,
|
||||
`pending=${String(params.trust.pending)}`,
|
||||
`stale=${String(params.trust.stale)}`,
|
||||
`reasons=${params.trust.reasons.length ? params.trust.reasons.join(", ") : "none"}`,
|
||||
];
|
||||
const riskSuffix =
|
||||
params.riskReasons.length > 0 ? ` Risk signals: ${params.riskReasons.join(", ")}.` : "";
|
||||
return `ClawHub trust warning for "${params.packageName}@${params.version}": ${details.join("; ")}.${riskSuffix}`;
|
||||
}
|
||||
|
||||
async function ensureClawHubPackageTrustAcknowledged(params: {
|
||||
packageName: string;
|
||||
version: string;
|
||||
baseUrl?: string;
|
||||
token?: string;
|
||||
timeoutMs?: number;
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => boolean | Promise<boolean>;
|
||||
logger?: PluginInstallLogger;
|
||||
}): Promise<ClawHubInstallFailure | null> {
|
||||
let trust: ClawHubPackageSecurityTrust;
|
||||
try {
|
||||
const security = await fetchClawHubPackageSecurity({
|
||||
name: params.packageName,
|
||||
version: params.version,
|
||||
baseUrl: params.baseUrl,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
trust = security.trust;
|
||||
} catch (error) {
|
||||
return buildClawHubInstallFailure(
|
||||
`ClawHub release trust check failed for "${params.packageName}@${params.version}": ${formatErrorMessage(error)}`,
|
||||
CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_SECURITY_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
|
||||
const riskReasons = resolveClawHubRiskReasons(trust);
|
||||
if (riskReasons.length === 0 && !trust.pending && !trust.stale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const warning = formatClawHubTrustWarning({
|
||||
packageName: params.packageName,
|
||||
version: params.version,
|
||||
trust,
|
||||
riskReasons,
|
||||
});
|
||||
params.logger?.warn?.(warning);
|
||||
if (riskReasons.length === 0 || params.acknowledgeClawHubRisk) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const acknowledged = params.onClawHubRisk
|
||||
? await params.onClawHubRisk({
|
||||
packageName: params.packageName,
|
||||
version: params.version,
|
||||
trust,
|
||||
warning,
|
||||
})
|
||||
: false;
|
||||
if (acknowledged) {
|
||||
return null;
|
||||
}
|
||||
return buildClawHubInstallFailure(
|
||||
`ClawHub release "${params.packageName}@${params.version}" has trust warnings. Review the package and rerun with --acknowledge-clawhub-risk to continue.`,
|
||||
CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_RISK_ACKNOWLEDGEMENT_REQUIRED,
|
||||
);
|
||||
}
|
||||
|
||||
function isMissingArtifactResolverRoute(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof ClawHubRequestError &&
|
||||
@@ -1055,6 +1181,8 @@ export async function installPluginFromClawHub(
|
||||
timeoutMs?: number;
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => boolean | Promise<boolean>;
|
||||
},
|
||||
): Promise<
|
||||
| ({
|
||||
@@ -1125,6 +1253,19 @@ export async function installPluginFromClawHub(
|
||||
compatibility: versionState.compatibility,
|
||||
logger: params.logger,
|
||||
});
|
||||
const trustFailure = await ensureClawHubPackageTrustAcknowledged({
|
||||
packageName: canonicalPackageName,
|
||||
version: versionState.version,
|
||||
baseUrl: params.baseUrl,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
acknowledgeClawHubRisk: params.acknowledgeClawHubRisk,
|
||||
onClawHubRisk: params.onClawHubRisk,
|
||||
logger: params.logger,
|
||||
});
|
||||
if (trustFailure) {
|
||||
return trustFailure;
|
||||
}
|
||||
|
||||
let archive;
|
||||
try {
|
||||
|
||||
@@ -2397,6 +2397,54 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards ClawHub risk acknowledgement inputs to dry-run and live ClawHub updates", async () => {
|
||||
const onClawHubRisk = vi.fn(async () => true);
|
||||
const config = createClawHubInstallConfig({
|
||||
pluginId: "demo",
|
||||
installPath: "/tmp/demo",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
});
|
||||
installPluginFromClawHubMock.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "demo",
|
||||
targetDir: "/tmp/demo",
|
||||
version: "1.2.4",
|
||||
clawhub: {
|
||||
source: "clawhub",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "demo",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
integrity: "sha256-next",
|
||||
resolvedAt: "2026-03-22T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
|
||||
for (const dryRun of [true, false]) {
|
||||
installPluginFromClawHubMock.mockClear();
|
||||
|
||||
await updateNpmInstalledPlugins({
|
||||
config,
|
||||
pluginIds: ["demo"],
|
||||
acknowledgeClawHubRisk: true,
|
||||
onClawHubRisk,
|
||||
...(dryRun ? { dryRun: true } : {}),
|
||||
});
|
||||
|
||||
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "clawhub:demo",
|
||||
acknowledgeClawHubRisk: true,
|
||||
onClawHubRisk,
|
||||
...(dryRun ? { dryRun: true } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("migrates legacy unscoped install keys when a scoped npm package updates", async () => {
|
||||
installPluginFromNpmSpecMock.mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -2947,9 +2995,12 @@ describe("syncPluginsForUpdateChannel", () => {
|
||||
clawhubPackage: "legacy-chat",
|
||||
}),
|
||||
);
|
||||
const onClawHubRisk = vi.fn(async () => true);
|
||||
|
||||
const result = await syncPluginsForUpdateChannel({
|
||||
channel: "stable",
|
||||
acknowledgeClawHubRisk: true,
|
||||
onClawHubRisk,
|
||||
externalizedBundledPluginBridges: [
|
||||
{
|
||||
bundledPluginId: "legacy-chat",
|
||||
@@ -2983,6 +3034,8 @@ describe("syncPluginsForUpdateChannel", () => {
|
||||
expect(clawHubInstallCall()?.baseUrl).toBe("https://clawhub.ai");
|
||||
expect(clawHubInstallCall()?.mode).toBe("update");
|
||||
expect(clawHubInstallCall()?.expectedPluginId).toBe("legacy-chat");
|
||||
expect(clawHubInstallCall()?.acknowledgeClawHubRisk).toBe(true);
|
||||
expect(clawHubInstallCall()?.onClawHubRisk).toBe(onClawHubRisk);
|
||||
expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.summary.switchedToClawHub).toEqual(["legacy-chat"]);
|
||||
|
||||
+22
-1
@@ -20,7 +20,11 @@ import type { UpdateChannel } from "../infra/update-channels.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveBundledPluginSources } from "./bundled-sources.js";
|
||||
import { buildClawHubPluginInstallRecordFields } from "./clawhub-install-records.js";
|
||||
import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "./clawhub.js";
|
||||
import {
|
||||
CLAWHUB_INSTALL_ERROR_CODE,
|
||||
installPluginFromClawHub,
|
||||
type ClawHubRiskAcknowledgementRequest,
|
||||
} from "./clawhub.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import {
|
||||
getExternalizedBundledPluginLegacyPathSuffix,
|
||||
@@ -848,6 +852,8 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
specOverrides?: Record<string, string>;
|
||||
onIntegrityDrift?: (params: PluginUpdateIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => boolean | Promise<boolean>;
|
||||
}): Promise<PluginUpdateSummary> {
|
||||
const logger = params.logger ?? {};
|
||||
const installs = params.config.plugins?.installs ?? {};
|
||||
@@ -866,6 +872,10 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
ranNpmInstaller = true;
|
||||
return await installPluginFromNpmSpec(installParams);
|
||||
};
|
||||
const clawHubRiskAcknowledgementOptions = {
|
||||
...(params.acknowledgeClawHubRisk ? { acknowledgeClawHubRisk: true } : {}),
|
||||
...(params.onClawHubRisk ? { onClawHubRisk: params.onClawHubRisk } : {}),
|
||||
};
|
||||
|
||||
const recordFailure = (pluginId: string, message: string) => {
|
||||
if (params.disableOnFailure && !params.dryRun) {
|
||||
@@ -1145,6 +1155,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
logger,
|
||||
})
|
||||
: record.source === "git"
|
||||
@@ -1232,6 +1243,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
@@ -1345,6 +1357,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
logger,
|
||||
})
|
||||
: record.source === "git"
|
||||
@@ -1428,6 +1441,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
@@ -1569,6 +1583,8 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
logger?: PluginUpdateLogger;
|
||||
externalizedBundledPluginBridges?: readonly ExternalizedBundledPluginBridge[];
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => boolean | Promise<boolean>;
|
||||
}): Promise<PluginChannelSyncResult> {
|
||||
const env = params.env ?? process.env;
|
||||
const logger = params.logger ?? {};
|
||||
@@ -1588,6 +1604,10 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
const loadHelpers = buildLoadPathHelpers(next.plugins?.load?.paths ?? [], env);
|
||||
let installs = next.plugins?.installs ?? {};
|
||||
let changed = false;
|
||||
const clawHubRiskAcknowledgementOptions = {
|
||||
...(params.acknowledgeClawHubRisk ? { acknowledgeClawHubRisk: true } : {}),
|
||||
...(params.onClawHubRisk ? { onClawHubRisk: params.onClawHubRisk } : {}),
|
||||
};
|
||||
|
||||
if (params.channel === "dev") {
|
||||
for (const [pluginId, record] of Object.entries(installs)) {
|
||||
@@ -1700,6 +1720,7 @@ export async function syncPluginsForUpdateChannel(params: {
|
||||
...(bridge.clawhubUrl ? { baseUrl: bridge.clawhubUrl } : {}),
|
||||
mode: "update",
|
||||
expectedPluginId: targetPluginId,
|
||||
...clawHubRiskAcknowledgementOptions,
|
||||
logger,
|
||||
});
|
||||
if (!result.ok && npmSpec && shouldFallbackClawHubBridgeToNpm(result)) {
|
||||
|
||||
Reference in New Issue
Block a user