From 6b003a7f2b43cf5ea1422982e5f64c3db51806f9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Apr 2026 12:35:46 +0900 Subject: [PATCH] refactor(cli): reuse install safety overrides --- src/cli/plugins-install-command.ts | 29 +++++++++++++--------- src/hooks/install.test.ts | 39 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/cli/plugins-install-command.ts b/src/cli/plugins-install-command.ts index 21e6f8bdb85..7ddf04f144c 100644 --- a/src/cli/plugins-install-command.ts +++ b/src/cli/plugins-install-command.ts @@ -42,6 +42,12 @@ function resolveInstallMode(force?: boolean): "install" | "update" { return force ? "update" : "install"; } +function resolveInstallSafetyOverrides(overrides: InstallSafetyOverrides): InstallSafetyOverrides { + return { + dangerouslyForceUnsafeInstall: overrides.dangerouslyForceUnsafeInstall, + }; +} + async function installBundledPluginSource(params: { config: OpenClawConfig; rawSpec: string; @@ -76,7 +82,7 @@ async function tryInstallHookPackFromLocalPath(params: { config: OpenClawConfig; resolvedPath: string; installMode: "install" | "update"; - dangerouslyForceUnsafeInstall?: boolean; + safetyOverrides?: InstallSafetyOverrides; link?: boolean; }): Promise<{ ok: true } | { ok: false; error: string }> { if (params.link) { @@ -89,7 +95,7 @@ async function tryInstallHookPackFromLocalPath(params: { } const probe = await installHooksFromPath({ - dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, + ...resolveInstallSafetyOverrides(params.safetyOverrides ?? {}), path: params.resolvedPath, dryRun: true, }); @@ -128,7 +134,7 @@ async function tryInstallHookPackFromLocalPath(params: { } const result = await installHooksFromPath({ - dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, + ...resolveInstallSafetyOverrides(params.safetyOverrides ?? {}), path: params.resolvedPath, mode: params.installMode, logger: createHookPackInstallLogger(), @@ -307,10 +313,11 @@ export async function runPluginInstallCommand(params: { return defaultRuntime.exit(1); } const installMode = resolveInstallMode(opts.force); + const safetyOverrides = resolveInstallSafetyOverrides(opts); if (opts.marketplace) { const result = await installPluginFromMarketplace({ - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, + ...safetyOverrides, marketplace: opts.marketplace, mode: installMode, plugin: raw, @@ -344,16 +351,16 @@ export async function runPluginInstallCommand(params: { const existing = cfg.plugins?.load?.paths ?? []; const merged = Array.from(new Set([...existing, resolved])); const probe = await installPluginFromPath({ - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, + ...safetyOverrides, path: resolved, dryRun: true, }); if (!probe.ok) { const hookFallback = await tryInstallHookPackFromLocalPath({ config: cfg, - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, installMode, resolvedPath: resolved, + safetyOverrides, link: true, }); if (hookFallback.ok) { @@ -389,7 +396,7 @@ export async function runPluginInstallCommand(params: { } const result = await installPluginFromPath({ - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, + ...safetyOverrides, mode: installMode, path: resolved, logger: createPluginInstallLogger(), @@ -397,9 +404,9 @@ export async function runPluginInstallCommand(params: { if (!result.ok) { const hookFallback = await tryInstallHookPackFromLocalPath({ config: cfg, - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, installMode, resolvedPath: resolved, + safetyOverrides, }); if (hookFallback.ok) { return; @@ -463,7 +470,7 @@ export async function runPluginInstallCommand(params: { const clawhubSpec = parseClawHubPluginSpec(raw); if (clawhubSpec) { const result = await installPluginFromClawHub({ - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, + ...safetyOverrides, mode: installMode, spec: raw, logger: createPluginInstallLogger(), @@ -499,7 +506,7 @@ export async function runPluginInstallCommand(params: { const preferredClawHubSpec = buildPreferredClawHubSpec(raw); if (preferredClawHubSpec) { const clawhubResult = await installPluginFromClawHub({ - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, + ...safetyOverrides, mode: installMode, spec: preferredClawHubSpec, logger: createPluginInstallLogger(), @@ -534,7 +541,7 @@ export async function runPluginInstallCommand(params: { } const result = await installPluginFromNpmSpec({ - dangerouslyForceUnsafeInstall: opts.dangerouslyForceUnsafeInstall, + ...safetyOverrides, mode: installMode, spec: raw, logger: createPluginInstallLogger(), diff --git a/src/hooks/install.test.ts b/src/hooks/install.test.ts index f340d588dc5..f305e4943f5 100644 --- a/src/hooks/install.test.ts +++ b/src/hooks/install.test.ts @@ -16,6 +16,7 @@ import { installHooksFromNpmSpec, installHooksFromPath, } from "./install.js"; +import * as hookInstallRuntime from "./install.runtime.js"; const fixtureRoot = path.join(process.cwd(), ".tmp", `openclaw-hook-install-${randomUUID()}`); const sharedArchiveDir = path.join(fixtureRoot, "_archives"); @@ -327,6 +328,44 @@ describe("installHooksFromPath", () => { }); describe("installHooksFromNpmSpec", () => { + it("forwards dangerous force unsafe install through npm-spec archive params", async () => { + const installFromValidatedNpmSpecArchiveSpy = vi + .spyOn(hookInstallRuntime, "installFromValidatedNpmSpecArchive") + .mockImplementation( + async ( + params: Parameters[0], + ) => { + expect(params.archiveInstallParams).toEqual( + expect.objectContaining({ + dangerouslyForceUnsafeInstall: true, + }), + ); + return { + ok: true, + hookPackId: "test-hooks", + hooks: ["one-hook"], + targetDir: "/tmp/hooks/test-hooks", + version: "0.0.1", + }; + }, + ); + + try { + const result = await installHooksFromNpmSpec({ + spec: "@openclaw/test-hooks@0.0.1", + dangerouslyForceUnsafeInstall: true, + }); + + expect(result.ok).toBe(true); + if (!result.ok) { + return; + } + expect(result.hookPackId).toBe("test-hooks"); + } finally { + installFromValidatedNpmSpecArchiveSpy.mockRestore(); + } + }); + it("uses --ignore-scripts for npm pack and cleans up temp dir", async () => { const stateDir = makeTempDir();