[codex] add Crestodian plugin management (#75869)

Summary:
- The branch adds ClawHub plugin search and Crestodian plugin list/search/install/uninstall flows, with docs, changelog, tests, runtime injection, and regenerated config baseline hashes.
- Reproducibility: not applicable. as a bug reproduction request. The high-confidence verification path is cur ... surface search plus exact-head diff/source inspection against the PR's targeted tests and queued CI checks.

ClawSweeper fixups:
- Included follow-up commit: Repair Crestodian plugin management config schema drift

Validation:
- ClawSweeper review passed for head c29cda6005.
- Required merge gates passed before the squash merge.

Prepared head SHA: c29cda6005
Review: https://github.com/openclaw/openclaw/pull/75869#issuecomment-4362360704

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-05-02 04:12:38 +01:00
committed by GitHub
parent 47f76c563f
commit eee3aeae00
20 changed files with 920 additions and 270 deletions

View File

@@ -22,7 +22,7 @@ import {
} from "../plugins/marketplace.js";
import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trace.js";
import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
import { defaultRuntime } from "../runtime.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { theme } from "../terminal/theme.js";
import { shortenHomePath } from "../utils.js";
import { looksLikeLocalInstallSpec } from "./install-spec.js";
@@ -110,6 +110,7 @@ async function installBundledPluginSource(params: {
rawSpec: string;
bundledSource: BundledPluginSource;
warning: string;
runtime?: RuntimeEnv;
}) {
const existingEntry = params.snapshot.config.plugins?.entries?.[params.bundledSource.pluginId];
const shouldEnable = hasValidBundledPluginConfig({
@@ -136,6 +137,7 @@ async function installBundledPluginSource(params: {
},
enable: shouldEnable,
warningMessage: [params.warning, configWarning].filter(Boolean).join("\n"),
runtime: params.runtime,
});
}
@@ -145,6 +147,7 @@ async function tryInstallHookPackFromLocalPath(params: {
installMode: "install" | "update";
safetyOverrides?: InstallSafetyOverrides;
link?: boolean;
runtime?: RuntimeEnv;
}): Promise<{ ok: true } | { ok: false; error: string }> {
if (params.link) {
const stat = fs.statSync(params.resolvedPath);
@@ -193,6 +196,7 @@ async function tryInstallHookPackFromLocalPath(params: {
version: probe.version,
},
successMessage: `Linked hook pack path: ${shortenHomePath(params.resolvedPath)}`,
runtime: params.runtime,
});
return { ok: true };
}
@@ -201,7 +205,7 @@ async function tryInstallHookPackFromLocalPath(params: {
...resolveInstallSafetyOverrides(params.safetyOverrides ?? {}),
path: params.resolvedPath,
mode: params.installMode,
logger: createHookPackInstallLogger(),
logger: createHookPackInstallLogger(params.runtime),
});
if (!result.ok) {
return result;
@@ -218,6 +222,7 @@ async function tryInstallHookPackFromLocalPath(params: {
installPath: result.targetDir,
version: result.version,
},
runtime: params.runtime,
});
return { ok: true };
}
@@ -227,11 +232,12 @@ async function tryInstallHookPackFromNpmSpec(params: {
installMode: "install" | "update";
spec: string;
pin?: boolean;
runtime?: RuntimeEnv;
}): Promise<{ ok: true } | { ok: false; error: string }> {
const result = await installHooksFromNpmSpec({
spec: params.spec,
mode: params.installMode,
logger: createHookPackInstallLogger(),
logger: createHookPackInstallLogger(params.runtime),
});
if (!result.ok) {
return result;
@@ -243,7 +249,7 @@ async function tryInstallHookPackFromNpmSpec(params: {
result.targetDir,
result.version,
result.npmResolution,
defaultRuntime.log,
params.runtime?.log ?? defaultRuntime.log,
theme.warn,
);
await persistHookPackInstall({
@@ -251,6 +257,7 @@ async function tryInstallHookPackFromNpmSpec(params: {
hookPackId: result.hookPackId,
hooks: result.hooks,
install: installRecord,
runtime: params.runtime,
});
return { ok: true };
}
@@ -263,17 +270,18 @@ async function tryInstallPluginOrHookPackFromNpmSpec(params: {
safetyOverrides: InstallSafetyOverrides;
allowBundledFallback: boolean;
extensionsDir: string;
runtime?: RuntimeEnv;
}): Promise<{ ok: true } | { ok: false }> {
const result = await installPluginFromNpmSpec({
...params.safetyOverrides,
mode: params.installMode,
spec: params.spec,
extensionsDir: params.extensionsDir,
logger: createPluginInstallLogger(),
logger: createPluginInstallLogger(params.runtime),
});
if (!result.ok) {
if (isTerminalPluginInstallSecurityFailure(result.code)) {
defaultRuntime.error(result.error);
(params.runtime ?? defaultRuntime).error(result.error);
return { ok: false };
}
if (params.allowBundledFallback) {
@@ -288,6 +296,7 @@ async function tryInstallPluginOrHookPackFromNpmSpec(params: {
rawSpec: params.spec,
bundledSource: bundledFallbackPlan.bundledSource,
warning: bundledFallbackPlan.warning,
runtime: params.runtime,
});
return { ok: true };
}
@@ -297,11 +306,12 @@ async function tryInstallPluginOrHookPackFromNpmSpec(params: {
installMode: params.installMode,
spec: params.spec,
pin: params.pin,
runtime: params.runtime,
});
if (hookFallback.ok) {
return { ok: true };
}
defaultRuntime.error(
(params.runtime ?? defaultRuntime).error(
formatPluginInstallWithHookFallbackError(result.error, hookFallback.error),
);
return { ok: false };
@@ -313,13 +323,14 @@ async function tryInstallPluginOrHookPackFromNpmSpec(params: {
result.targetDir,
result.version,
result.npmResolution,
defaultRuntime.log,
params.runtime?.log ?? defaultRuntime.log,
theme.warn,
);
await persistPluginInstall({
snapshot: params.snapshot,
pluginId: result.pluginId,
install: installRecord,
runtime: params.runtime,
});
return { ok: true };
}
@@ -330,16 +341,17 @@ async function tryInstallPluginFromGitSpec(params: {
spec: string;
safetyOverrides: InstallSafetyOverrides;
extensionsDir: string;
runtime?: RuntimeEnv;
}): Promise<{ ok: true } | { ok: false }> {
const result = await installPluginFromGitSpec({
...params.safetyOverrides,
mode: params.installMode,
spec: params.spec,
extensionsDir: params.extensionsDir,
logger: createPluginInstallLogger(),
logger: createPluginInstallLogger(params.runtime),
});
if (!result.ok) {
defaultRuntime.error(result.error);
(params.runtime ?? defaultRuntime).error(result.error);
return { ok: false };
}
@@ -356,6 +368,7 @@ async function tryInstallPluginFromGitSpec(params: {
gitRef: result.git.ref,
gitCommit: result.git.commit,
},
runtime: params.runtime,
});
return { ok: true };
}
@@ -452,7 +465,9 @@ export async function runPluginInstallCommand(params: {
pin?: boolean;
marketplace?: string;
};
runtime?: RuntimeEnv;
}) {
const runtime = params.runtime ?? defaultRuntime;
const shorthand = !params.opts.marketplace
? await tracePluginLifecyclePhaseAsync(
"marketplace shortcut resolution",
@@ -461,8 +476,8 @@ export async function runPluginInstallCommand(params: {
)
: null;
if (shorthand?.ok === false) {
defaultRuntime.error(shorthand.error);
return defaultRuntime.exit(1);
runtime.error(shorthand.error);
return runtime.exit(1);
}
const raw = shorthand?.ok ? shorthand.plugin : params.raw;
@@ -473,47 +488,47 @@ export async function runPluginInstallCommand(params: {
};
if (opts.marketplace) {
if (opts.link) {
defaultRuntime.error("`--link` is not supported with `--marketplace`.");
return defaultRuntime.exit(1);
runtime.error("`--link` is not supported with `--marketplace`.");
return runtime.exit(1);
}
if (opts.pin) {
defaultRuntime.error("`--pin` is not supported with `--marketplace`.");
return defaultRuntime.exit(1);
runtime.error("`--pin` is not supported with `--marketplace`.");
return runtime.exit(1);
}
}
const gitPrefix = raw.trim().toLowerCase().startsWith("git:");
const gitSpec = parseGitPluginSpec(raw);
if (gitPrefix && !gitSpec) {
defaultRuntime.error(`unsupported git: plugin spec: ${raw}`);
return defaultRuntime.exit(1);
runtime.error(`unsupported git: plugin spec: ${raw}`);
return runtime.exit(1);
}
if (gitSpec && opts.link) {
defaultRuntime.error("`--link` is not supported with `git:` installs.");
return defaultRuntime.exit(1);
runtime.error("`--link` is not supported with `git:` installs.");
return runtime.exit(1);
}
if (gitSpec && opts.pin) {
defaultRuntime.error("`--pin` is not supported with `git:` installs; use `git:<repo>@<ref>`.");
return defaultRuntime.exit(1);
runtime.error("`--pin` is not supported with `git:` installs; use `git:<repo>@<ref>`.");
return runtime.exit(1);
}
if (opts.link && opts.force) {
defaultRuntime.error("`--force` is not supported with `--link`.");
return defaultRuntime.exit(1);
runtime.error("`--force` is not supported with `--link`.");
return runtime.exit(1);
}
const requestResolution = resolvePluginInstallRequestContext({
rawSpec: raw,
marketplace: opts.marketplace,
});
if (!requestResolution.ok) {
defaultRuntime.error(requestResolution.error);
return defaultRuntime.exit(1);
runtime.error(requestResolution.error);
return runtime.exit(1);
}
const request = requestResolution.request;
const snapshot = await loadConfigForInstall(request).catch((error: unknown) => {
defaultRuntime.error(formatErrorMessage(error));
runtime.error(formatErrorMessage(error));
return null;
});
if (!snapshot) {
return defaultRuntime.exit(1);
return runtime.exit(1);
}
const cfg = snapshot.config;
const installMode = resolveInstallMode(opts.force);
@@ -527,11 +542,11 @@ export async function runPluginInstallCommand(params: {
mode: installMode,
plugin: raw,
extensionsDir,
logger: createPluginInstallLogger(),
logger: createPluginInstallLogger(runtime),
});
if (!result.ok) {
defaultRuntime.error(result.error);
return defaultRuntime.exit(1);
runtime.error(result.error);
return runtime.exit(1);
}
await persistPluginInstall({
@@ -545,6 +560,7 @@ export async function runPluginInstallCommand(params: {
marketplaceSource: result.marketplaceSource,
marketplacePlugin: result.marketplacePlugin,
},
runtime,
});
return;
}
@@ -560,12 +576,12 @@ export async function runPluginInstallCommand(params: {
path: resolved,
dryRun: true,
extensionsDir,
logger: createPluginInstallLogger(),
logger: createPluginInstallLogger(runtime),
});
if (!probe.ok) {
if (isTerminalPluginInstallSecurityFailure(probe.code)) {
defaultRuntime.error(probe.error);
return defaultRuntime.exit(1);
runtime.error(probe.error);
return runtime.exit(1);
}
const hookFallback = await tryInstallHookPackFromLocalPath({
snapshot,
@@ -573,14 +589,13 @@ export async function runPluginInstallCommand(params: {
resolvedPath: resolved,
safetyOverrides,
link: true,
runtime,
});
if (hookFallback.ok) {
return;
}
defaultRuntime.error(
formatPluginInstallWithHookFallbackError(probe.error, hookFallback.error),
);
return defaultRuntime.exit(1);
runtime.error(formatPluginInstallWithHookFallbackError(probe.error, hookFallback.error));
return runtime.exit(1);
}
await persistPluginInstall({
@@ -605,6 +620,7 @@ export async function runPluginInstallCommand(params: {
version: probe.version,
},
successMessage: `Linked plugin path: ${shortenHomePath(resolved)}`,
runtime,
});
return;
}
@@ -614,26 +630,25 @@ export async function runPluginInstallCommand(params: {
mode: installMode,
path: resolved,
extensionsDir,
logger: createPluginInstallLogger(),
logger: createPluginInstallLogger(runtime),
});
if (!result.ok) {
if (isTerminalPluginInstallSecurityFailure(result.code)) {
defaultRuntime.error(result.error);
return defaultRuntime.exit(1);
runtime.error(result.error);
return runtime.exit(1);
}
const hookFallback = await tryInstallHookPackFromLocalPath({
snapshot,
installMode,
resolvedPath: resolved,
safetyOverrides,
runtime,
});
if (hookFallback.ok) {
return;
}
defaultRuntime.error(
formatPluginInstallWithHookFallbackError(result.error, hookFallback.error),
);
return defaultRuntime.exit(1);
runtime.error(formatPluginInstallWithHookFallbackError(result.error, hookFallback.error));
return runtime.exit(1);
}
const source: "archive" | "path" = resolveArchiveKind(resolved) ? "archive" : "path";
@@ -646,20 +661,21 @@ export async function runPluginInstallCommand(params: {
installPath: result.targetDir,
version: result.version,
},
runtime,
});
return;
}
if (opts.link) {
defaultRuntime.error("`--link` requires a local path.");
return defaultRuntime.exit(1);
runtime.error("`--link` requires a local path.");
return runtime.exit(1);
}
const npmPrefixSpec = parseNpmPrefixSpec(raw);
if (npmPrefixSpec !== null) {
if (!npmPrefixSpec) {
defaultRuntime.error("unsupported npm: spec: missing package");
return defaultRuntime.exit(1);
runtime.error("unsupported npm: spec: missing package");
return runtime.exit(1);
}
const npmPrefixResult = await tryInstallPluginOrHookPackFromNpmSpec({
snapshot,
@@ -669,9 +685,10 @@ export async function runPluginInstallCommand(params: {
safetyOverrides,
allowBundledFallback: false,
extensionsDir,
runtime,
});
if (!npmPrefixResult.ok) {
return defaultRuntime.exit(1);
return runtime.exit(1);
}
return;
}
@@ -683,9 +700,10 @@ export async function runPluginInstallCommand(params: {
spec: raw,
safetyOverrides,
extensionsDir,
runtime,
});
if (!gitResult.ok) {
return defaultRuntime.exit(1);
return runtime.exit(1);
}
return;
}
@@ -702,8 +720,8 @@ export async function runPluginInstallCommand(params: {
".zip",
])
) {
defaultRuntime.error(`Path not found: ${resolved}`);
return defaultRuntime.exit(1);
runtime.error(`Path not found: ${resolved}`);
return runtime.exit(1);
}
const bundledPreNpmPlan = resolveBundledInstallPlanBeforeNpm({
@@ -719,6 +737,7 @@ export async function runPluginInstallCommand(params: {
rawSpec: raw,
bundledSource: bundledPreNpmPlan.bundledSource,
warning: bundledPreNpmPlan.warning,
runtime,
}),
{
command: "install",
@@ -736,11 +755,11 @@ export async function runPluginInstallCommand(params: {
mode: installMode,
spec: raw,
extensionsDir,
logger: createPluginInstallLogger(),
logger: createPluginInstallLogger(runtime),
});
if (!result.ok) {
defaultRuntime.error(result.error);
return defaultRuntime.exit(1);
runtime.error(result.error);
return runtime.exit(1);
}
await persistPluginInstall({
@@ -762,6 +781,7 @@ export async function runPluginInstallCommand(params: {
clawpackManifestSha256: result.clawhub.clawpackManifestSha256,
clawpackSize: result.clawhub.clawpackSize,
},
runtime,
});
return;
}
@@ -773,7 +793,7 @@ export async function runPluginInstallCommand(params: {
mode: installMode,
spec: preferredClawHubSpec,
extensionsDir,
logger: createPluginInstallLogger(),
logger: createPluginInstallLogger(runtime),
});
if (clawhubResult.ok) {
await persistPluginInstall({
@@ -795,12 +815,13 @@ export async function runPluginInstallCommand(params: {
clawpackManifestSha256: clawhubResult.clawhub.clawpackManifestSha256,
clawpackSize: clawhubResult.clawhub.clawpackSize,
},
runtime,
});
return;
}
if (decidePreferredClawHubFallback(clawhubResult) !== "fallback_to_npm") {
defaultRuntime.error(clawhubResult.error);
return defaultRuntime.exit(1);
runtime.error(clawhubResult.error);
return runtime.exit(1);
}
}
@@ -812,8 +833,9 @@ export async function runPluginInstallCommand(params: {
safetyOverrides,
allowBundledFallback: true,
extensionsDir,
runtime,
});
if (!npmResult.ok) {
return defaultRuntime.exit(1);
return runtime.exit(1);
}
}