diff --git a/src/agents/tools/crestodian-tool.ts b/src/agents/tools/crestodian-tool.ts index c8f575789226..083eb0a4d836 100644 --- a/src/agents/tools/crestodian-tool.ts +++ b/src/agents/tools/crestodian-tool.ts @@ -9,9 +9,9 @@ import { Type } from "typebox"; import { executeCrestodianOperation, isPersistentCrestodianOperation, - validateCrestodianPluginInstallSpec, type CrestodianOperation, } from "../../crestodian/operations.js"; +import { validateCrestodianPluginInstallSpec } from "../../crestodian/plugin-install.js"; import type { RuntimeEnv } from "../../runtime.js"; import { stringEnum } from "../schema/typebox.js"; import { stableStringify } from "../stable-stringify.js"; diff --git a/src/auto-reply/reply/commands-plugins-install.ts b/src/auto-reply/reply/commands-plugins-install.ts new file mode 100644 index 000000000000..9d908ba9b533 --- /dev/null +++ b/src/auto-reply/reply/commands-plugins-install.ts @@ -0,0 +1,323 @@ +import fs from "node:fs"; +import { stripAnsi } from "../../../packages/terminal-core/src/ansi.js"; +import { buildNpmInstallRecordFields } from "../../cli/npm-resolution.js"; +import { resolveBundledInstallPlanBeforeNpm } from "../../cli/plugin-install-plan.js"; +import { + createPluginInstallLogger, + parseNpmPackPrefixPath, + resolveFileNpmSpecToLocalPath, +} from "../../cli/plugins-command-helpers.js"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { PluginInstallRecord } from "../../config/types.plugins.js"; +import { resolveArchiveKind } from "../../infra/archive.js"; +import { parseClawHubPluginSpec } from "../../infra/clawhub.js"; +import { installBundledPluginSource } from "../../plugins/bundled-install.js"; +import { findBundledPluginSource } from "../../plugins/bundled-sources.js"; +import { buildClawHubPluginInstallRecordFields } from "../../plugins/clawhub-install-records.js"; +import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../plugins/clawhub.js"; +import { installPluginFromGitSpec, parseGitPluginSpec } from "../../plugins/git-install.js"; +import { + persistPluginInstall, + type ConfigSnapshotForInstallPersist, +} from "../../plugins/install-persistence.js"; +import { + formatNonClawHubInstallWarning, + NON_CLAWHUB_INSTALL_FORCE_FLAG, + resolveOpenClawTrustedNpmPackageInstall, + type NonClawHubInstallSourceClass, +} from "../../plugins/install-provenance.js"; +import { + installPluginFromNpmPackArchive, + installPluginFromNpmSpec, + installPluginFromPath, +} from "../../plugins/install.js"; +import { resolveCatalogOfficialExternalInstallPlan } from "../../plugins/official-external-install-trust.js"; +import { resolveUserPath } from "../../utils.js"; + +function looksLikeLocalPluginInstallSpec(raw: string): boolean { + return ( + raw.startsWith(".") || + raw.startsWith("~") || + raw.startsWith("/") || + raw.endsWith(".ts") || + raw.endsWith(".js") || + raw.endsWith(".mjs") || + raw.endsWith(".cjs") || + raw.endsWith(".tgz") || + raw.endsWith(".tar.gz") || + raw.endsWith(".tar") || + raw.endsWith(".zip") + ); +} + +function resolveNonClawHubChatInstallAcknowledgement(params: { + force: boolean; + sourceClass: NonClawHubInstallSourceClass; + spec: string; +}): { ok: true; warning: string } | { ok: false; error: string } { + const warning = formatNonClawHubInstallWarning(params); + if (params.force) { + return { ok: true, warning }; + } + return { + ok: false, + error: `${warning}\nReview the source, then rerun this chat command with ${NON_CLAWHUB_INSTALL_FORCE_FLAG} to continue.`, + }; +} + +export async function installPluginFromPluginsCommand(params: { + raw: string; + force: boolean; + config: OpenClawConfig; + snapshot: ConfigSnapshotForInstallPersist; +}): Promise< + { ok: true; pluginId: string; warnings?: readonly string[] } | { ok: false; error: string } +> { + const fileSpec = resolveFileNpmSpecToLocalPath(params.raw); + if (fileSpec && !fileSpec.ok) { + return { ok: false, error: fileSpec.error }; + } + const normalized = fileSpec && fileSpec.ok ? fileSpec.path : params.raw; + const resolved = resolveUserPath(normalized); + const installMode = params.force ? "update" : "install"; + + if (fs.existsSync(resolved)) { + const source: "archive" | "path" = resolveArchiveKind(resolved) ? "archive" : "path"; + const bundledLocalSource = + source === "path" + ? findBundledPluginSource({ lookup: { kind: "localPath", value: resolved } }) + : undefined; + const acknowledgement = bundledLocalSource + ? null + : resolveNonClawHubChatInstallAcknowledgement({ + force: params.force, + sourceClass: source === "archive" ? "local-archive" : "local-path", + spec: params.raw, + }); + if (acknowledgement && !acknowledgement.ok) { + return acknowledgement; + } + const result = await installPluginFromPath({ + path: resolved, + config: params.config, + mode: installMode, + logger: createPluginInstallLogger(), + }); + if (!result.ok) { + return { ok: false, error: result.error }; + } + await persistPluginInstall({ + snapshot: params.snapshot, + pluginId: result.pluginId, + install: { + source, + sourcePath: resolved, + installPath: result.targetDir, + version: result.version, + }, + }); + return { + ok: true, + pluginId: result.pluginId, + ...(acknowledgement?.ok ? { warnings: [acknowledgement.warning] } : {}), + }; + } + + const npmPackPath = parseNpmPackPrefixPath(params.raw); + if (npmPackPath !== null) { + if (!npmPackPath) { + return { ok: false, error: "Unsupported npm-pack plugin spec: missing archive path." }; + } + const acknowledgement = resolveNonClawHubChatInstallAcknowledgement({ + force: params.force, + sourceClass: "npm-pack", + spec: params.raw, + }); + if (!acknowledgement.ok) { + return acknowledgement; + } + const result = await installPluginFromNpmPackArchive({ + archivePath: npmPackPath, + config: params.config, + mode: installMode, + logger: createPluginInstallLogger(), + }); + if (!result.ok) { + return { ok: false, error: result.error }; + } + const installRecord = { + ...buildNpmInstallRecordFields({ + spec: result.npmResolution?.resolvedSpec ?? result.manifestName ?? result.pluginId, + installPath: result.targetDir, + version: result.version, + resolution: result.npmResolution, + }), + sourcePath: npmPackPath, + artifactKind: "npm-pack", + artifactFormat: "tgz", + ...(result.npmResolution?.integrity ? { npmIntegrity: result.npmResolution.integrity } : {}), + ...(result.npmResolution?.shasum ? { npmShasum: result.npmResolution.shasum } : {}), + ...(result.npmTarballName ? { npmTarballName: result.npmTarballName } : {}), + } satisfies PluginInstallRecord; + await persistPluginInstall({ + snapshot: params.snapshot, + pluginId: result.pluginId, + install: installRecord, + }); + return { ok: true, pluginId: result.pluginId, warnings: [acknowledgement.warning] }; + } + + if (looksLikeLocalPluginInstallSpec(params.raw)) { + return { ok: false, error: `Path not found: ${resolved}` }; + } + + const gitPrefix = params.raw.trim().toLowerCase().startsWith("git:"); + const gitSpec = parseGitPluginSpec(params.raw); + if (gitPrefix && !gitSpec) { + return { ok: false, error: `unsupported git: plugin spec: ${params.raw}` }; + } + if (gitSpec) { + const acknowledgement = resolveNonClawHubChatInstallAcknowledgement({ + force: params.force, + sourceClass: "git", + spec: params.raw, + }); + if (!acknowledgement.ok) { + return acknowledgement; + } + const result = await installPluginFromGitSpec({ + spec: params.raw, + config: params.config, + mode: installMode, + logger: createPluginInstallLogger(), + }); + if (!result.ok) { + return { ok: false, error: result.error }; + } + await persistPluginInstall({ + snapshot: params.snapshot, + pluginId: result.pluginId, + install: { + source: "git", + spec: params.raw, + installPath: result.targetDir, + version: result.version, + resolvedAt: result.git.resolvedAt, + gitUrl: result.git.url, + gitRef: result.git.ref, + gitCommit: result.git.commit, + }, + }); + return { ok: true, pluginId: result.pluginId, warnings: [acknowledgement.warning] }; + } + + const clawhubSpec = parseClawHubPluginSpec(params.raw); + if (clawhubSpec) { + const warnings: string[] = []; + const logger = createPluginInstallLogger(); + const result = await installPluginFromClawHub({ + spec: params.raw, + config: params.config, + mode: installMode, + logger: { + info: logger.info, + warn: (message) => { + warnings.push(stripAnsi(message)); + logger.warn(message); + }, + terminalLinks: false, + }, + }); + if (!result.ok) { + const warning = "warning" in result ? result.warning : warnings.join("\n"); + const warningPrefix = warning ? `${warning} ` : ""; + if (result.code === CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_RISK_ACKNOWLEDGEMENT_REQUIRED) { + return { + ok: false, + error: `${warningPrefix}${result.error} The /plugins chat command cannot acknowledge ClawHub risk; run the local openclaw plugins install command with --acknowledge-clawhub-risk from a trusted shell after reviewing the warning.`, + }; + } + return { ok: false, error: `${warningPrefix}${result.error}` }; + } + await persistPluginInstall({ + snapshot: params.snapshot, + pluginId: result.pluginId, + install: { + ...buildClawHubPluginInstallRecordFields(result.clawhub), + spec: params.raw, + installPath: result.targetDir, + version: result.version, + }, + }); + return { ok: true, pluginId: result.pluginId, warnings }; + } + + const npmSpec = params.raw.trim().toLowerCase().startsWith("npm:") + ? params.raw.trim().slice("npm:".length) + : params.raw; + const explicitNpm = params.raw.trim().toLowerCase().startsWith("npm:"); + const bundledPlan = explicitNpm + ? null + : resolveBundledInstallPlanBeforeNpm({ + rawSpec: params.raw, + findBundledSource: (lookup) => findBundledPluginSource({ lookup }), + }); + if (bundledPlan) { + const bundledInstall = await installBundledPluginSource({ + snapshot: params.snapshot, + rawSpec: params.raw, + bundledSource: bundledPlan.bundledSource, + warning: bundledPlan.warning, + }); + return { + ok: true, + pluginId: bundledInstall.pluginId, + warnings: bundledInstall.warnings, + }; + } + const trustedNpmInstall = resolveOpenClawTrustedNpmPackageInstall(npmSpec); + const officialIdPlan = resolveCatalogOfficialExternalInstallPlan(params.raw); + const arbitraryNpmAcknowledgement = + !trustedNpmInstall && !officialIdPlan + ? resolveNonClawHubChatInstallAcknowledgement({ + force: params.force, + sourceClass: "npm", + spec: params.raw, + }) + : null; + if (arbitraryNpmAcknowledgement && !arbitraryNpmAcknowledgement.ok) { + return arbitraryNpmAcknowledgement; + } + const trustedPluginId = trustedNpmInstall?.pluginId ?? officialIdPlan?.pluginId; + const trustedNpmSpec = officialIdPlan?.npmSpec ?? npmSpec; + const expectedIntegrity = + trustedNpmInstall?.expectedIntegrity ?? officialIdPlan?.expectedIntegrity; + const result = await installPluginFromNpmSpec({ + spec: trustedNpmSpec, + config: params.config, + mode: installMode, + ...(trustedPluginId ? { expectedPluginId: trustedPluginId } : {}), + ...(expectedIntegrity ? { expectedIntegrity } : {}), + ...(trustedNpmInstall || officialIdPlan ? { trustedSourceLinkedOfficialInstall: true } : {}), + logger: createPluginInstallLogger(), + }); + if (!result.ok) { + return { ok: false, error: result.error }; + } + const installRecord = buildNpmInstallRecordFields({ + spec: trustedNpmSpec, + installPath: result.targetDir, + version: result.version, + resolution: result.npmResolution, + }); + await persistPluginInstall({ + snapshot: params.snapshot, + pluginId: result.pluginId, + install: installRecord, + }); + return { + ok: true, + pluginId: result.pluginId, + ...(arbitraryNpmAcknowledgement?.ok ? { warnings: [arbitraryNpmAcknowledgement.warning] } : {}), + }; +} diff --git a/src/auto-reply/reply/commands-plugins.ts b/src/auto-reply/reply/commands-plugins.ts index 785535df2cc7..59076431c6da 100644 --- a/src/auto-reply/reply/commands-plugins.ts +++ b/src/auto-reply/reply/commands-plugins.ts @@ -1,45 +1,16 @@ -// Implements plugin command listing, install, and configuration helpers. -import fs from "node:fs"; +// Implements plugin command listing and configuration helpers. import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; -import { stripAnsi } from "../../../packages/terminal-core/src/ansi.js"; -import { buildNpmInstallRecordFields } from "../../cli/npm-resolution.js"; -import { resolveBundledInstallPlanBeforeNpm } from "../../cli/plugin-install-plan.js"; -import { - createPluginInstallLogger, - parseNpmPackPrefixPath, - resolveFileNpmSpecToLocalPath, -} from "../../cli/plugins-command-helpers.js"; import { readConfigFileSnapshot, readConfigFileSnapshotForWrite } from "../../config/config.js"; import { assertConfigWriteAllowedInCurrentMode } from "../../config/nix-mode-write-guard.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { PluginInstallRecord } from "../../config/types.plugins.js"; -import { resolveArchiveKind } from "../../infra/archive.js"; -import { parseClawHubPluginSpec } from "../../infra/clawhub.js"; import { formatErrorMessage } from "../../infra/errors.js"; -import { installBundledPluginSource } from "../../plugins/bundled-install.js"; -import { findBundledPluginSource } from "../../plugins/bundled-sources.js"; -import { buildClawHubPluginInstallRecordFields } from "../../plugins/clawhub-install-records.js"; -import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../plugins/clawhub.js"; -import { installPluginFromGitSpec, parseGitPluginSpec } from "../../plugins/git-install.js"; import { - persistPluginInstall, resolveInstallConfigMutationPreflights, selectInstallMutationWriteOptions, type ConfigSnapshotForInstallPersist, } from "../../plugins/install-persistence.js"; -import { - formatNonClawHubInstallWarning, - NON_CLAWHUB_INSTALL_FORCE_FLAG, - resolveOpenClawTrustedNpmPackageInstall, - type NonClawHubInstallSourceClass, -} from "../../plugins/install-provenance.js"; -import { - installPluginFromNpmPackArchive, - installPluginFromNpmSpec, - installPluginFromPath, -} from "../../plugins/install.js"; import { loadInstalledPluginIndexInstallRecords } from "../../plugins/installed-plugin-index-records.js"; -import { resolveCatalogOfficialExternalInstallPlan } from "../../plugins/official-external-install-trust.js"; import { refreshPluginRegistryAfterConfigMutation } from "../../plugins/registry-refresh.js"; import type { PluginRecord } from "../../plugins/registry.js"; import { @@ -50,13 +21,13 @@ import { formatPluginCompatibilityNotice, type PluginStatusReport, } from "../../plugins/status.js"; -import { resolveUserPath } from "../../utils.js"; import { rejectNonOwnerCommand, rejectUnauthorizedCommand, requireCommandFlagEnabled, requireGatewayClientScope, } from "./command-gates.js"; +import { installPluginFromPluginsCommand } from "./commands-plugins-install.js"; import type { CommandHandler } from "./commands-types.js"; import { AutoReplyConfigMutationError, setPluginEnabledFromCommand } from "./config-mutations.js"; import { parsePluginsCommand } from "./plugins-commands.js"; @@ -185,294 +156,6 @@ function findPlugin(report: PluginStatusReport, rawName: string): PluginRecord | ); } -function looksLikeLocalPluginInstallSpec(raw: string): boolean { - return ( - raw.startsWith(".") || - raw.startsWith("~") || - raw.startsWith("/") || - raw.endsWith(".ts") || - raw.endsWith(".js") || - raw.endsWith(".mjs") || - raw.endsWith(".cjs") || - raw.endsWith(".tgz") || - raw.endsWith(".tar.gz") || - raw.endsWith(".tar") || - raw.endsWith(".zip") - ); -} - -function resolveNonClawHubChatInstallAcknowledgement(params: { - force: boolean; - sourceClass: NonClawHubInstallSourceClass; - spec: string; -}): { ok: true; warning: string } | { ok: false; error: string } { - const warning = formatNonClawHubInstallWarning(params); - if (params.force) { - return { ok: true, warning }; - } - return { - ok: false, - error: `${warning}\nReview the source, then rerun this chat command with ${NON_CLAWHUB_INSTALL_FORCE_FLAG} to continue.`, - }; -} - -async function installPluginFromPluginsCommand(params: { - raw: string; - force: boolean; - config: OpenClawConfig; - snapshot: ConfigSnapshotForInstallPersist; -}): Promise< - { ok: true; pluginId: string; warnings?: readonly string[] } | { ok: false; error: string } -> { - const fileSpec = resolveFileNpmSpecToLocalPath(params.raw); - if (fileSpec && !fileSpec.ok) { - return { ok: false, error: fileSpec.error }; - } - const normalized = fileSpec && fileSpec.ok ? fileSpec.path : params.raw; - const resolved = resolveUserPath(normalized); - const installMode = params.force ? "update" : "install"; - - if (fs.existsSync(resolved)) { - const source: "archive" | "path" = resolveArchiveKind(resolved) ? "archive" : "path"; - const bundledLocalSource = - source === "path" - ? findBundledPluginSource({ lookup: { kind: "localPath", value: resolved } }) - : undefined; - const acknowledgement = bundledLocalSource - ? null - : resolveNonClawHubChatInstallAcknowledgement({ - force: params.force, - sourceClass: source === "archive" ? "local-archive" : "local-path", - spec: params.raw, - }); - if (acknowledgement && !acknowledgement.ok) { - return acknowledgement; - } - const result = await installPluginFromPath({ - path: resolved, - config: params.config, - mode: installMode, - logger: createPluginInstallLogger(), - }); - if (!result.ok) { - return { ok: false, error: result.error }; - } - await persistPluginInstall({ - snapshot: params.snapshot, - pluginId: result.pluginId, - install: { - source, - sourcePath: resolved, - installPath: result.targetDir, - version: result.version, - }, - }); - return { - ok: true, - pluginId: result.pluginId, - ...(acknowledgement?.ok ? { warnings: [acknowledgement.warning] } : {}), - }; - } - - const npmPackPath = parseNpmPackPrefixPath(params.raw); - if (npmPackPath !== null) { - if (!npmPackPath) { - return { ok: false, error: "Unsupported npm-pack plugin spec: missing archive path." }; - } - const acknowledgement = resolveNonClawHubChatInstallAcknowledgement({ - force: params.force, - sourceClass: "npm-pack", - spec: params.raw, - }); - if (!acknowledgement.ok) { - return acknowledgement; - } - const result = await installPluginFromNpmPackArchive({ - archivePath: npmPackPath, - config: params.config, - mode: installMode, - logger: createPluginInstallLogger(), - }); - if (!result.ok) { - return { ok: false, error: result.error }; - } - const installRecord = { - ...buildNpmInstallRecordFields({ - spec: result.npmResolution?.resolvedSpec ?? result.manifestName ?? result.pluginId, - installPath: result.targetDir, - version: result.version, - resolution: result.npmResolution, - }), - sourcePath: npmPackPath, - artifactKind: "npm-pack", - artifactFormat: "tgz", - ...(result.npmResolution?.integrity ? { npmIntegrity: result.npmResolution.integrity } : {}), - ...(result.npmResolution?.shasum ? { npmShasum: result.npmResolution.shasum } : {}), - ...(result.npmTarballName ? { npmTarballName: result.npmTarballName } : {}), - } satisfies PluginInstallRecord; - await persistPluginInstall({ - snapshot: params.snapshot, - pluginId: result.pluginId, - install: installRecord, - }); - return { ok: true, pluginId: result.pluginId, warnings: [acknowledgement.warning] }; - } - - if (looksLikeLocalPluginInstallSpec(params.raw)) { - return { ok: false, error: `Path not found: ${resolved}` }; - } - - const gitPrefix = params.raw.trim().toLowerCase().startsWith("git:"); - const gitSpec = parseGitPluginSpec(params.raw); - if (gitPrefix && !gitSpec) { - return { ok: false, error: `unsupported git: plugin spec: ${params.raw}` }; - } - if (gitSpec) { - const acknowledgement = resolveNonClawHubChatInstallAcknowledgement({ - force: params.force, - sourceClass: "git", - spec: params.raw, - }); - if (!acknowledgement.ok) { - return acknowledgement; - } - const result = await installPluginFromGitSpec({ - spec: params.raw, - config: params.config, - mode: installMode, - logger: createPluginInstallLogger(), - }); - if (!result.ok) { - return { ok: false, error: result.error }; - } - await persistPluginInstall({ - snapshot: params.snapshot, - pluginId: result.pluginId, - install: { - source: "git", - spec: params.raw, - installPath: result.targetDir, - version: result.version, - resolvedAt: result.git.resolvedAt, - gitUrl: result.git.url, - gitRef: result.git.ref, - gitCommit: result.git.commit, - }, - }); - return { ok: true, pluginId: result.pluginId, warnings: [acknowledgement.warning] }; - } - - const clawhubSpec = parseClawHubPluginSpec(params.raw); - if (clawhubSpec) { - const warnings: string[] = []; - const logger = createPluginInstallLogger(); - const result = await installPluginFromClawHub({ - spec: params.raw, - config: params.config, - mode: installMode, - logger: { - info: logger.info, - warn: (message) => { - warnings.push(stripAnsi(message)); - logger.warn(message); - }, - terminalLinks: false, - }, - }); - if (!result.ok) { - const warning = "warning" in result ? result.warning : warnings.join("\n"); - const warningPrefix = warning ? `${warning} ` : ""; - if (result.code === CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_RISK_ACKNOWLEDGEMENT_REQUIRED) { - return { - ok: false, - error: `${warningPrefix}${result.error} The /plugins chat command cannot acknowledge ClawHub risk; run the local openclaw plugins install command with --acknowledge-clawhub-risk from a trusted shell after reviewing the warning.`, - }; - } - return { ok: false, error: `${warningPrefix}${result.error}` }; - } - await persistPluginInstall({ - snapshot: params.snapshot, - pluginId: result.pluginId, - install: { - ...buildClawHubPluginInstallRecordFields(result.clawhub), - spec: params.raw, - installPath: result.targetDir, - version: result.version, - }, - }); - return { ok: true, pluginId: result.pluginId, warnings }; - } - - const npmSpec = params.raw.trim().toLowerCase().startsWith("npm:") - ? params.raw.trim().slice("npm:".length) - : params.raw; - const explicitNpm = params.raw.trim().toLowerCase().startsWith("npm:"); - const bundledPlan = explicitNpm - ? null - : resolveBundledInstallPlanBeforeNpm({ - rawSpec: params.raw, - findBundledSource: (lookup) => findBundledPluginSource({ lookup }), - }); - if (bundledPlan) { - const bundledInstall = await installBundledPluginSource({ - snapshot: params.snapshot, - rawSpec: params.raw, - bundledSource: bundledPlan.bundledSource, - warning: bundledPlan.warning, - }); - return { - ok: true, - pluginId: bundledInstall.pluginId, - warnings: bundledInstall.warnings, - }; - } - const trustedNpmInstall = resolveOpenClawTrustedNpmPackageInstall(npmSpec); - const officialIdPlan = resolveCatalogOfficialExternalInstallPlan(params.raw); - const arbitraryNpmAcknowledgement = - !trustedNpmInstall && !officialIdPlan - ? resolveNonClawHubChatInstallAcknowledgement({ - force: params.force, - sourceClass: "npm", - spec: params.raw, - }) - : null; - if (arbitraryNpmAcknowledgement && !arbitraryNpmAcknowledgement.ok) { - return arbitraryNpmAcknowledgement; - } - const trustedPluginId = trustedNpmInstall?.pluginId ?? officialIdPlan?.pluginId; - const trustedNpmSpec = officialIdPlan?.npmSpec ?? npmSpec; - const expectedIntegrity = - trustedNpmInstall?.expectedIntegrity ?? officialIdPlan?.expectedIntegrity; - const result = await installPluginFromNpmSpec({ - spec: trustedNpmSpec, - config: params.config, - mode: installMode, - ...(trustedPluginId ? { expectedPluginId: trustedPluginId } : {}), - ...(expectedIntegrity ? { expectedIntegrity } : {}), - ...(trustedNpmInstall || officialIdPlan ? { trustedSourceLinkedOfficialInstall: true } : {}), - logger: createPluginInstallLogger(), - }); - if (!result.ok) { - return { ok: false, error: result.error }; - } - const installRecord = buildNpmInstallRecordFields({ - spec: trustedNpmSpec, - installPath: result.targetDir, - version: result.version, - resolution: result.npmResolution, - }); - await persistPluginInstall({ - snapshot: params.snapshot, - pluginId: result.pluginId, - install: installRecord, - }); - return { - ok: true, - pluginId: result.pluginId, - ...(arbitraryNpmAcknowledgement?.ok ? { warnings: [arbitraryNpmAcknowledgement.warning] } : {}), - }; -} - async function loadPluginCommandState( workspaceDir: string, options?: { loadModules?: boolean }, diff --git a/src/cli/hooks-cli.ts b/src/cli/hooks-cli.ts index da90ca16c7f1..81b5f6835884 100644 --- a/src/cli/hooks-cli.ts +++ b/src/cli/hooks-cli.ts @@ -1,4 +1,3 @@ -// Hooks CLI for listing, checking, toggling, installing, and updating hook integrations. import type { Command } from "commander"; import { decorativeEmoji, @@ -47,12 +46,6 @@ type HooksUpdateOptions = { dryRun?: boolean; }; -type HooksInstallOptions = { - force?: boolean; - link?: boolean; - pin?: boolean; -}; - function mergeHookEntries(pluginEntries: HookEntry[], workspaceEntries: HookEntry[]): HookEntry[] { return resolveHookEntries([...pluginEntries, ...workspaceEntries]); } @@ -571,15 +564,11 @@ export function registerHooksCli(program: Command): void { .option("-l, --link", "Link a local path instead of copying", false) .option("--pin", "Record npm installs as exact resolved @", false) .option("--force", "Confirm non-ClawHub sources and overwrite an existing hook pack", false) - .action(async (raw: string, opts: HooksInstallOptions) => { + .action(async (raw: string, opts: { force?: boolean; link?: boolean; pin?: boolean }) => { defaultRuntime.log( theme.warn("`openclaw hooks install` is deprecated; use `openclaw plugins install`."), ); - await runPluginInstallCommand({ - raw, - opts, - invalidateRuntimeCache: false, - }); + await runPluginInstallCommand({ raw, opts, invalidateRuntimeCache: false }); }); hooks diff --git a/src/cli/plugins-install-command.ts b/src/cli/plugins-install-command.ts index ec24ce667f52..7b1c62443b3c 100644 --- a/src/cli/plugins-install-command.ts +++ b/src/cli/plugins-install-command.ts @@ -122,10 +122,6 @@ async function probeHookPackFromPath( const DEPRECATED_DANGEROUS_FORCE_UNSAFE_INSTALL_WARNING = "--dangerously-force-unsafe-install is deprecated and no longer affects plugin installs because built-in install-time dangerous-code scanning has been removed. Configure security.installPolicy for operator-owned install decisions."; -function isEmptyRecord(value: Record): boolean { - return Object.keys(value).length === 0; -} - function supportsPluginRecoveryIncludeShape(parsed: Record): boolean { if (Object.hasOwn(parsed, "$include")) { return false; diff --git a/src/crestodian/operations.ts b/src/crestodian/operations.ts index c2b416f3ef4e..cd02eadd2c26 100644 --- a/src/crestodian/operations.ts +++ b/src/crestodian/operations.ts @@ -4,7 +4,6 @@ import type { ConfigSetOptions } from "../cli/config-set-input.js"; import type { DoctorOptions } from "../commands/doctor.types.js"; import { isSensitiveConfigPath } from "../config/sensitive-paths.js"; import { formatErrorMessage } from "../infra/errors.js"; -import { isOpenClawTrustedPluginInstallSpec } from "../plugins/install-provenance.js"; import { buildAgentMainSessionKey, normalizeAgentId } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import type { TuiResult } from "../tui/tui-types.js"; @@ -16,6 +15,7 @@ import { type DefaultInferenceRouteProjection, } from "./inference-route.js"; import type { CrestodianOverview } from "./overview.js"; +import { validateCrestodianPluginInstallSpec } from "./plugin-install.js"; /** * Crestodian command parser and operation executor. @@ -420,24 +420,6 @@ function normalizePluginInstallSpec(spec: string, source: string | undefined): s return trimmed; } -export function validateCrestodianPluginInstallSpec(spec: string): string | null { - const trimmed = spec.trim(); - if (!trimmed) { - return "Plugin install spec is required."; - } - if (/\s/.test(trimmed)) { - return "Crestodian plugin install accepts one npm or ClawHub package spec."; - } - if (/^(?:\.{1,2}\/|\/|~\/|file:|git(?:\+ssh|\+https)?:|https?:)/i.test(trimmed)) { - // Crestodian does not install local paths or URLs; those can execute arbitrary package code. - return "Crestodian plugin install accepts npm or ClawHub package specs only."; - } - if (!isOpenClawTrustedPluginInstallSpec(trimmed)) { - return "Crestodian installs only ClawHub, bundled, or official-catalog plugins. Use `openclaw plugins install ` in a trusted shell to review an arbitrary executable source."; - } - return null; -} - /** * Return whether an operation can change local state or process lifecycle. * Guided setup operations are intentionally absent: starting a wizard is not diff --git a/src/crestodian/plugin-install.ts b/src/crestodian/plugin-install.ts new file mode 100644 index 000000000000..83da3bed11cb --- /dev/null +++ b/src/crestodian/plugin-install.ts @@ -0,0 +1,19 @@ +import { isOpenClawTrustedPluginInstallSpec } from "../plugins/install-provenance.js"; + +export function validateCrestodianPluginInstallSpec(spec: string): string | null { + const trimmed = spec.trim(); + if (!trimmed) { + return "Plugin install spec is required."; + } + if (/\s/.test(trimmed)) { + return "Crestodian plugin install accepts one npm or ClawHub package spec."; + } + if (/^(?:\.{1,2}\/|\/|~\/|file:|git(?:\+ssh|\+https)?:|https?:)/i.test(trimmed)) { + // Crestodian does not install local paths or URLs; those can execute arbitrary package code. + return "Crestodian plugin install accepts npm or ClawHub package specs only."; + } + if (!isOpenClawTrustedPluginInstallSpec(trimmed)) { + return "Crestodian installs only ClawHub, bundled, or official-catalog plugins. Use `openclaw plugins install ` in a trusted shell to review an arbitrary executable source."; + } + return null; +}