mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 10:41:38 +00:00
381 lines
13 KiB
TypeScript
381 lines
13 KiB
TypeScript
// Executes validated plugin, marketplace, ClawHub, and hook-pack install requests.
|
|
import { theme } from "../../packages/terminal-core/src/theme.js";
|
|
import { assertConfigWriteAllowedInCurrentMode } from "../config/config.js";
|
|
import { parseClawHubPluginSpec, reportClawHubPluginInstallTelemetry } from "../infra/clawhub.js";
|
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js";
|
|
import { resolveDefaultPluginExtensionsDir } from "../plugins/install-paths.js";
|
|
import { persistPluginInstall } from "../plugins/install-persistence.js";
|
|
import { installManagedPluginSource } from "../plugins/management-service.js";
|
|
import { installPluginFromMarketplace } from "../plugins/marketplace.js";
|
|
import { withPluginLifecycleLease } from "../plugins/plugin-lifecycle-lease.js";
|
|
import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trace.js";
|
|
import { defaultRuntime } from "../runtime.js";
|
|
import { markClawPackageIndependentlyOwned } from "../state/claw-package-adoption.js";
|
|
import { withClawPackageLifecycleLease } from "../state/claw-package-lifecycle-lease.js";
|
|
import { shortenHomePath } from "../utils.js";
|
|
import { resolveClawHubRiskAcknowledgementCliOptions } from "./clawhub-risk-acknowledgement.js";
|
|
import {
|
|
confirmNonClawHubInstall,
|
|
type NonClawHubInstallSourceClass,
|
|
} from "./non-clawhub-install-acknowledgement.js";
|
|
import {
|
|
createPluginInstallLogger,
|
|
formatPluginInstallWithHookFallbackError,
|
|
} from "./plugins-command-helpers.js";
|
|
import {
|
|
loadConfigForInstall,
|
|
resolveFullyBlockedConfigMutationReason,
|
|
} from "./plugins-install-config.js";
|
|
import {
|
|
isTerminalPluginInstallFailure,
|
|
probeHookPackFromPath,
|
|
resolveInstallSafetyOverrides,
|
|
tryInstallHookPackFromLocalPath,
|
|
tryInstallPluginOrHookPackFromNpmSpec,
|
|
} from "./plugins-install-hook-fallback.js";
|
|
import {
|
|
resolvePluginInstallPreflight,
|
|
type PluginInstallPreflight,
|
|
type RunPluginInstallCommandParams,
|
|
} from "./plugins-install-preflight.js";
|
|
|
|
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 isClawHubBlockedCliFailure(result: { code?: string; warning?: string }): boolean {
|
|
return (
|
|
result.code === CLAWHUB_INSTALL_ERROR_CODE.CLAWHUB_DOWNLOAD_BLOCKED &&
|
|
typeof result.warning === "string" &&
|
|
result.warning.trim().length > 0
|
|
);
|
|
}
|
|
|
|
/** Validate install intent before opening the SQLite-backed plugin lifecycle lease. */
|
|
export async function runPluginInstallCommand(params: RunPluginInstallCommandParams) {
|
|
assertConfigWriteAllowedInCurrentMode();
|
|
const runtime = params.runtime ?? defaultRuntime;
|
|
const preflight = await resolvePluginInstallPreflight(params);
|
|
if (!preflight.ok) {
|
|
runtime.error(preflight.error);
|
|
return runtime.exit(1);
|
|
}
|
|
return await withPluginLifecycleLease(
|
|
{},
|
|
async () => await runPluginInstallCommandUnlocked(params, preflight),
|
|
);
|
|
}
|
|
|
|
async function runPluginInstallCommandUnlocked(
|
|
params: RunPluginInstallCommandParams,
|
|
preflight: Extract<PluginInstallPreflight, { ok: true }>,
|
|
) {
|
|
assertConfigWriteAllowedInCurrentMode();
|
|
|
|
const runtime = params.runtime ?? defaultRuntime;
|
|
const invalidateRuntimeCache = params.invalidateRuntimeCache ?? true;
|
|
const { raw, opts, installMode, request } = preflight;
|
|
if (opts.dangerouslyForceUnsafeInstall) {
|
|
runtime.log(theme.warn(DEPRECATED_DANGEROUS_FORCE_UNSAFE_INSTALL_WARNING));
|
|
}
|
|
|
|
const snapshot = await loadConfigForInstall(request).catch((error: unknown) => {
|
|
runtime.error(formatErrorMessage(error));
|
|
return null;
|
|
});
|
|
if (!snapshot) {
|
|
return runtime.exit(1);
|
|
}
|
|
const safetyOverrides = resolveInstallSafetyOverrides({ ...opts, config: snapshot.config });
|
|
const acknowledgeNonClawHubSource = async (
|
|
sourceClass: NonClawHubInstallSourceClass,
|
|
spec: string,
|
|
): Promise<boolean> =>
|
|
await confirmNonClawHubInstall({
|
|
acknowledged: opts.force,
|
|
runtime,
|
|
sourceClass,
|
|
spec,
|
|
});
|
|
|
|
if (preflight.sourcePlan === null) {
|
|
if (
|
|
!(await acknowledgeNonClawHubSource("marketplace", `${raw} from ${preflight.marketplace}`))
|
|
) {
|
|
return runtime.exit(1);
|
|
}
|
|
const result = await installPluginFromMarketplace({
|
|
...safetyOverrides,
|
|
marketplace: preflight.marketplace,
|
|
mode: installMode,
|
|
plugin: raw,
|
|
extensionsDir: resolveDefaultPluginExtensionsDir(),
|
|
logger: createPluginInstallLogger(runtime),
|
|
});
|
|
if (!result.ok) {
|
|
if (!isClawHubBlockedCliFailure(result)) {
|
|
runtime.error(result.error);
|
|
}
|
|
return runtime.exit(1);
|
|
}
|
|
|
|
await persistPluginInstall({
|
|
snapshot,
|
|
pluginId: result.pluginId,
|
|
install: {
|
|
source: "marketplace",
|
|
installPath: result.targetDir,
|
|
version: result.version,
|
|
marketplaceName: result.marketplaceName,
|
|
marketplaceSource: result.marketplaceSource,
|
|
marketplacePlugin: result.marketplacePlugin,
|
|
},
|
|
invalidateRuntimeCache,
|
|
runtime,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const { sourcePlan } = preflight;
|
|
if (
|
|
sourcePlan.acknowledgement &&
|
|
!(await acknowledgeNonClawHubSource(
|
|
sourcePlan.acknowledgement.sourceClass,
|
|
sourcePlan.acknowledgement.spec,
|
|
))
|
|
) {
|
|
return runtime.exit(1);
|
|
}
|
|
|
|
const sourceRequest = sourcePlan.request;
|
|
switch (sourceRequest.source) {
|
|
case "local": {
|
|
const resolved = sourceRequest.path;
|
|
if (sourceRequest.link) {
|
|
sourceRequest.successMessage = `Linked plugin path: ${shortenHomePath(resolved)}`;
|
|
}
|
|
const fullyBlockedReason = resolveFullyBlockedConfigMutationReason(snapshot);
|
|
if (fullyBlockedReason) {
|
|
runtime.error(fullyBlockedReason);
|
|
return runtime.exit(1);
|
|
}
|
|
if (snapshot.pluginMutation.mode === "blocked" || snapshot.hookMutation.mode === "blocked") {
|
|
const hookProbe = await probeHookPackFromPath({
|
|
...safetyOverrides,
|
|
path: resolved,
|
|
mode: installMode,
|
|
inspection: "package-kind",
|
|
});
|
|
if (hookProbe.ok && hookProbe.packageKind === "hook-only") {
|
|
if (snapshot.hookMutation.mode === "blocked") {
|
|
runtime.error(snapshot.hookMutation.reason);
|
|
return runtime.exit(1);
|
|
}
|
|
const hookFallback = await tryInstallHookPackFromLocalPath({
|
|
snapshot,
|
|
installMode,
|
|
resolvedPath: resolved,
|
|
safetyOverrides,
|
|
...(opts.link ? { link: true } : {}),
|
|
expectedPackageKind: "hook-only",
|
|
runtime,
|
|
});
|
|
if (hookFallback.ok) {
|
|
return;
|
|
}
|
|
runtime.error(hookFallback.error);
|
|
return runtime.exit(1);
|
|
}
|
|
if (snapshot.pluginMutation.mode === "blocked") {
|
|
runtime.error(snapshot.pluginMutation.reason);
|
|
return runtime.exit(1);
|
|
}
|
|
}
|
|
|
|
const result = await installManagedPluginSource({
|
|
request: sourceRequest,
|
|
snapshot,
|
|
safetyOverrides,
|
|
logger: createPluginInstallLogger(runtime),
|
|
invalidateRuntimeCache,
|
|
runtime,
|
|
});
|
|
if (result.ok) {
|
|
return;
|
|
}
|
|
if (isTerminalPluginInstallFailure(result.code)) {
|
|
runtime.error(result.error);
|
|
return runtime.exit(1);
|
|
}
|
|
const hookFallback = await tryInstallHookPackFromLocalPath({
|
|
snapshot,
|
|
installMode,
|
|
resolvedPath: resolved,
|
|
safetyOverrides,
|
|
...(sourceRequest.link ? { link: true } : {}),
|
|
runtime,
|
|
});
|
|
if (hookFallback.ok) {
|
|
return;
|
|
}
|
|
runtime.error(formatPluginInstallWithHookFallbackError(result.error, hookFallback));
|
|
return runtime.exit(1);
|
|
}
|
|
|
|
case "npm-pack":
|
|
case "git": {
|
|
const result = await installManagedPluginSource({
|
|
request: sourceRequest,
|
|
snapshot,
|
|
safetyOverrides,
|
|
logger: createPluginInstallLogger(runtime),
|
|
invalidateRuntimeCache,
|
|
runtime,
|
|
});
|
|
if (!result.ok) {
|
|
runtime.error(result.error);
|
|
return runtime.exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
case "bundled": {
|
|
const result = await tracePluginLifecyclePhaseAsync(
|
|
"install execution",
|
|
() =>
|
|
installManagedPluginSource({
|
|
request: sourceRequest,
|
|
snapshot,
|
|
invalidateRuntimeCache,
|
|
runtime,
|
|
}),
|
|
{
|
|
command: "install",
|
|
source: "bundled",
|
|
pluginId: sourceRequest.bundledSource.pluginId,
|
|
},
|
|
);
|
|
if (!result.ok) {
|
|
runtime.error(result.error);
|
|
return runtime.exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
case "official": {
|
|
const result = await tryInstallPluginOrHookPackFromNpmSpec({
|
|
snapshot,
|
|
installMode,
|
|
spec: sourceRequest.spec,
|
|
pin: sourceRequest.pin,
|
|
safetyOverrides,
|
|
allowBundledFallback: false,
|
|
expectedPluginId: sourceRequest.pluginId,
|
|
expectedIntegrity: sourceRequest.expectedIntegrity,
|
|
trustedSourceLinkedOfficialInstall: true,
|
|
official: true,
|
|
invalidateRuntimeCache,
|
|
runtime,
|
|
});
|
|
if (!result.ok) {
|
|
return runtime.exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
case "clawhub": {
|
|
const installFromClawHub = async (
|
|
installSnapshot = snapshot,
|
|
installSafetyOverrides = safetyOverrides,
|
|
) => {
|
|
const acknowledgement = resolveClawHubRiskAcknowledgementCliOptions({
|
|
acknowledgeClawHubRisk: opts.acknowledgeClawHubRisk,
|
|
action: "installing",
|
|
});
|
|
const result = await installManagedPluginSource({
|
|
request: {
|
|
...sourceRequest,
|
|
...(opts.expectedIntegrity ? { expectedIntegrity: opts.expectedIntegrity } : {}),
|
|
...(opts.expectedPluginId ? { expectedPluginId: opts.expectedPluginId } : {}),
|
|
...(acknowledgement.acknowledgeClawHubRisk ? { acknowledgeClawHubRisk: true } : {}),
|
|
...(acknowledgement.onClawHubRisk
|
|
? { onClawHubRisk: acknowledgement.onClawHubRisk }
|
|
: {}),
|
|
},
|
|
snapshot: installSnapshot,
|
|
safetyOverrides: installSafetyOverrides,
|
|
logger: createPluginInstallLogger(runtime),
|
|
invalidateRuntimeCache,
|
|
runtime,
|
|
});
|
|
if (!result.ok) {
|
|
if (!isClawHubBlockedCliFailure(result)) {
|
|
runtime.error(result.error);
|
|
}
|
|
return runtime.exit(1);
|
|
}
|
|
if (!result.clawhub) {
|
|
runtime.error("ClawHub plugin install completed without source metadata.");
|
|
return runtime.exit(1);
|
|
}
|
|
|
|
if (!params.clawManaged && result.clawhub.version) {
|
|
markClawPackageIndependentlyOwned({
|
|
kind: "plugin",
|
|
source: "clawhub",
|
|
ref: result.clawhub.clawhubPackage,
|
|
version: result.clawhub.version,
|
|
});
|
|
}
|
|
await reportClawHubPluginInstallTelemetry({
|
|
baseUrl: result.clawhub.clawhubUrl,
|
|
packageName: result.clawhub.clawhubPackage,
|
|
version: result.clawhub.version,
|
|
}).catch(() => undefined);
|
|
};
|
|
if (params.clawManaged) {
|
|
return await installFromClawHub();
|
|
}
|
|
return await withClawPackageLifecycleLease(
|
|
{
|
|
kind: "plugin",
|
|
source: "clawhub",
|
|
ref: parseClawHubPluginSpec(sourceRequest.spec)?.name ?? sourceRequest.spec,
|
|
},
|
|
async () => {
|
|
const leasedSnapshot = await loadConfigForInstall(request).catch((error: unknown) => {
|
|
runtime.error(formatErrorMessage(error));
|
|
return null;
|
|
});
|
|
if (!leasedSnapshot) {
|
|
return runtime.exit(1);
|
|
}
|
|
return await installFromClawHub(
|
|
leasedSnapshot,
|
|
resolveInstallSafetyOverrides({ ...opts, config: leasedSnapshot.config }),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
case "npm": {
|
|
const result = await tryInstallPluginOrHookPackFromNpmSpec({
|
|
snapshot,
|
|
installMode,
|
|
spec: sourceRequest.spec,
|
|
pin: sourceRequest.pin,
|
|
safetyOverrides,
|
|
allowBundledFallback: sourceRequest.allowBundledFallback ?? false,
|
|
invalidateRuntimeCache,
|
|
expectedPluginId: sourceRequest.expectedPluginId,
|
|
expectedIntegrity: sourceRequest.expectedIntegrity,
|
|
trustedSourceLinkedOfficialInstall: sourceRequest.trustedSourceLinkedOfficialInstall,
|
|
runtime,
|
|
});
|
|
if (!result.ok) {
|
|
return runtime.exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|