mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
refactor(plugins): split install policy helpers
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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] } : {}),
|
||||
};
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
+2
-13
@@ -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 <name>@<version>", 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
|
||||
|
||||
@@ -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<string, unknown>): boolean {
|
||||
return Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
function supportsPluginRecoveryIncludeShape(parsed: Record<string, unknown>): boolean {
|
||||
if (Object.hasOwn(parsed, "$include")) {
|
||||
return false;
|
||||
|
||||
@@ -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 <spec>` 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
|
||||
|
||||
@@ -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 <spec>` in a trusted shell to review an arbitrary executable source.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user