refactor(plugins): split install policy helpers

This commit is contained in:
Jesse Merhi
2026-07-14 16:49:46 +10:00
parent 4433080882
commit ef440b5eb9
7 changed files with 348 additions and 356 deletions
+1 -1
View File
@@ -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] } : {}),
};
}
+2 -319
View File
@@ -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
View File
@@ -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
-4
View File
@@ -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;
+1 -19
View File
@@ -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
+19
View File
@@ -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;
}