Skip to content

Commit eee3aea

Browse files
[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 c29cda6. - Required merge gates passed before the squash merge. Prepared head SHA: c29cda6 Review: #75869 (comment) Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
1 parent 47f76c5 commit eee3aea

20 files changed

Lines changed: 920 additions & 270 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313
- Plugins/ClawHub: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verifying the ClawPack response header and downloaded bytes before installing. Thanks @vincentkoc.
1414
- Plugins/ClawHub: persist ClawPack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc.
1515
- Plugins/ClawHub: allow official bundled-plugin cutovers to prefer ClawHub installs with npm fallback only when the ClawHub package or version is absent. Thanks @vincentkoc.
16+
- Plugins/Crestodian: add ClawHub plugin search plus Crestodian plugin list/search/install/uninstall operations, with approval and audit coverage for install and uninstall.
1617
- Providers/OpenAI: add `extraBody`/`extra_body` passthrough for OpenAI-compatible TTS endpoints, so custom speech servers can receive fields such as `lang` in `/audio/speech` requests. Fixes #39900. Thanks @R3NK0R.
1718
- Dependencies: refresh workspace dependency pins, including TypeBox 1.1.37, AWS SDK 3.1041.0, Microsoft Teams 2.0.9, and Marked 18.0.3. Thanks @mariozechner, @aws, and @microsoft.
1819
- Discord/channels: add reusable message-channel access groups plus Discord channel-audience DM authorization, so allowlists can reference `accessGroup:<name>` across channel auth paths. (#75813)
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
35224e7970e71225a51482432f1618ae3b54be9615956d8554a0e2df3d263bc8 config-baseline.json
2-
80e6e8dce647aef2d1310de55a81d27de52cca47fc24bd7ad81b80f43a72b84c config-baseline.core.json
3-
eab8a85eefa2792fb8b98a07698e5ec31ff0b6f8af6222767e8049dcc5c4f529 config-baseline.channel.json
4-
af71b84b2411d8ccabcc6e09de0ee41f8212ff9869a6677698b6e7e3afdfaa47 config-baseline.plugin.json
1+
ae25cb1d397f1ea9642047ef13d35300c807cb1cd67f681c0b5af83b572b3638 config-baseline.json
2+
0a1907d595765b8bb7a41348d14323920ab50e402be49a19a45a4e2499306407 config-baseline.core.json
3+
c401cd3450f1737bc92418cfea301d20b54b7fbef9e6049834acc01af338e538 config-baseline.channel.json
4+
7731a0b93cb335b56fac4c807447ba659fea51ea7a6cd844dc0ef5616669ee75 config-baseline.plugin.json

docs/cli/crestodian.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ agents
7171
create agent work workspace ~/Projects/work
7272
models
7373
set default model openai/gpt-5.5
74+
plugins list
75+
plugins search slack
76+
plugin install clawhub:openclaw-codex-app-server
77+
plugin uninstall openclaw-codex-app-server
7478
talk to work agent
7579
talk to agent for ~/Projects/work
7680
audit
@@ -99,6 +103,8 @@ Read-only operations can run immediately:
99103

100104
- show overview
101105
- list agents
106+
- list installed plugins
107+
- search ClawHub plugins
102108
- show model/backend status
103109
- run status or health checks
104110
- check Gateway reachability
@@ -116,6 +122,8 @@ you pass `--yes` for a direct command:
116122
- change the default model
117123
- start, stop, or restart the Gateway
118124
- create agents
125+
- install plugins from ClawHub or npm
126+
- uninstall plugins
119127
- run doctor repairs that rewrite config or state
120128

121129
Applied writes are recorded in:
@@ -240,6 +248,9 @@ Security contract for remote rescue:
240248
- Require an explicit owner identity. Rescue must not accept wildcard sender
241249
rules, open group policy, unauthenticated webhooks, or anonymous channels.
242250
- Owner DMs only by default. Group/channel rescue requires explicit opt-in.
251+
- Plugin search and list are read-only. Plugin install is local-only by default
252+
because it downloads executable code. Plugin uninstall can be allowed as an
253+
approved repair operation when rescue policy permits persistent writes.
243254
- Remote rescue cannot open the local TUI or switch into an interactive agent
244255
session. Use local `openclaw` for agent handoff.
245256
- Persistent writes still require approval, even in rescue mode.

docs/cli/plugins.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ openclaw plugins list
3131
openclaw plugins list --enabled
3232
openclaw plugins list --verbose
3333
openclaw plugins list --json
34+
openclaw plugins search <query>
35+
openclaw plugins search <query> --limit 20
36+
openclaw plugins search <query> --json
3437
openclaw plugins install <path-or-spec>
3538
openclaw plugins inspect <id>
3639
openclaw plugins inspect <id> --runtime
@@ -64,6 +67,7 @@ Native OpenClaw plugins must ship `openclaw.plugin.json` with an inline JSON Sch
6467
### Install
6568

6669
```bash
70+
openclaw plugins search "calendar" # search ClawHub plugins
6771
openclaw plugins install <package> # ClawHub first, then npm
6872
openclaw plugins install clawhub:<package> # ClawHub only
6973
openclaw plugins install npm:<package> # npm only
@@ -82,6 +86,10 @@ openclaw plugins install <plugin> --marketplace https://github.com/<owner>/<repo
8286
Bare package names are checked against ClawHub first, then npm. Treat plugin installs like running code. Prefer pinned versions.
8387
</Warning>
8488

89+
`plugins search` queries ClawHub for installable plugin packages and prints
90+
install-ready package names. It searches code-plugin and bundle-plugin packages,
91+
not skills. Use `openclaw skills search` for ClawHub skills.
92+
8593
<Note>
8694
ClawHub is the primary distribution and discovery surface for most plugins. Npm
8795
remains a supported fallback and direct-install path. During the migration to
@@ -217,6 +225,9 @@ openclaw plugins list
217225
openclaw plugins list --enabled
218226
openclaw plugins list --verbose
219227
openclaw plugins list --json
228+
openclaw plugins search <query>
229+
openclaw plugins search <query> --limit 20
230+
openclaw plugins search <query> --json
220231
```
221232

222233
<ParamField path="--enabled" type="boolean">
@@ -233,6 +244,11 @@ openclaw plugins list --json
233244
`plugins list` reads the persisted local plugin registry first, with a manifest-only derived fallback when the registry is missing or invalid. It is useful for checking whether a plugin is installed, enabled, and visible to cold startup planning, but it is not a live runtime probe of an already-running Gateway process. After changing plugin code, enablement, hook policy, or `plugins.load.paths`, restart the Gateway that serves the channel before expecting new `register(api)` code or hooks to run. For remote/container deployments, verify you are restarting the actual `openclaw gateway run` child, not only a wrapper process.
234245
</Note>
235246

247+
`plugins search` is a remote ClawHub catalog lookup. It does not inspect local
248+
state, mutate config, install packages, or load plugin runtime code. Search
249+
results include the ClawHub package name, family, channel, version, summary, and
250+
an install hint such as `openclaw plugins install clawhub:<package>`.
251+
236252
For bundled plugin work inside a packaged Docker image, bind-mount the plugin
237253
source directory over the matching packaged source path, such as
238254
`/app/extensions/synology-chat`. OpenClaw will discover that mounted source

docs/tools/clawhub.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@ Site: [clawhub.ai](https://clawhub.ai)
6060
</Tab>
6161
<Tab title="Plugins">
6262
```bash
63+
openclaw plugins search "calendar"
6364
openclaw plugins install clawhub:<package>
6465
openclaw plugins update --all
6566
```
6667

67-
Bare npm-safe plugin specs are also tried against ClawHub before npm:
68+
`plugins search` queries the ClawHub plugin catalog and prints install-ready
69+
package names. Bare npm-safe plugin specs are also tried against ClawHub
70+
before npm:
6871

6972
```bash
7073
openclaw plugins install openclaw-codex-app-server

docs/tools/plugin.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ temporary set of OpenClaw-owned plugin packages while that migration finishes.
2727

2828
<Step title="Install a plugin">
2929
```bash
30+
# Search ClawHub plugins
31+
openclaw plugins search "calendar"
32+
33+
# From ClawHub
34+
openclaw plugins install clawhub:openclaw-codex-app-server
35+
3036
# From npm
3137
openclaw plugins install npm:@acme/openclaw-plugin
3238

@@ -433,6 +439,7 @@ openclaw plugins list # compact inventory
433439
openclaw plugins list --enabled # only enabled plugins
434440
openclaw plugins list --verbose # per-plugin detail lines
435441
openclaw plugins list --json # machine-readable inventory
442+
openclaw plugins search <query> # search ClawHub plugin catalog
436443
openclaw plugins inspect <id> # static detail
437444
openclaw plugins inspect <id> --runtime # registered hooks/tools/CLI/gateway methods
438445
openclaw plugins inspect <id> --json # machine-readable

src/cli/plugins-cli-test-helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ function restoreRuntimeCaptureMocks() {
151151

152152
vi.mock("../runtime.js", () => ({
153153
defaultRuntime,
154+
writeRuntimeJson: (runtime: CliMockOutputRuntime, value: unknown, space = 2) =>
155+
runtime.writeJson(value, space),
154156
}));
155157

156158
vi.mock("../config/config.js", () => ({

src/cli/plugins-cli.ts

Lines changed: 19 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import os from "node:os";
2-
import path from "node:path";
31
import type { Command } from "commander";
42
import { getRuntimeConfig, readConfigFileSnapshot, replaceConfigFile } from "../config/config.js";
5-
import { resolveStateDir } from "../config/paths.js";
63
import type { OpenClawConfig } from "../config/types.openclaw.js";
7-
import {
8-
tracePluginLifecyclePhase,
9-
tracePluginLifecyclePhaseAsync,
10-
} from "../plugins/plugin-lifecycle-trace.js";
4+
import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trace.js";
115
import { defaultRuntime } from "../runtime.js";
126
import { formatDocsLink } from "../terminal/links.js";
137
import { theme } from "../terminal/theme.js";
14-
import { shortenHomePath } from "../utils.js";
158
import type { PluginInspectOptions } from "./plugins-inspect-command.js";
169
import type { PluginsListOptions } from "./plugins-list-command.js";
1710
import { applyParentDefaultHelpAction } from "./program/parent-default-help.js";
@@ -26,6 +19,11 @@ export type PluginMarketplaceListOptions = {
2619
json?: boolean;
2720
};
2821

22+
export type PluginSearchOptions = {
23+
json?: boolean;
24+
limit?: number;
25+
};
26+
2927
export type PluginUninstallOptions = {
3028
keepFiles?: boolean;
3129
/** @deprecated Use keepFiles. */
@@ -74,6 +72,17 @@ export function registerPluginsCli(program: Command) {
7472
await runPluginsListCommand(opts);
7573
});
7674

75+
plugins
76+
.command("search")
77+
.description("Search ClawHub plugin packages")
78+
.argument("[query...]", "Search query")
79+
.option("--limit <n>", "Max results", (value) => Number.parseInt(value, 10))
80+
.option("--json", "Print JSON", false)
81+
.action(async (queryParts: string[], opts: PluginSearchOptions) => {
82+
const { runPluginsSearchCommand } = await import("./plugins-search-command.js");
83+
await runPluginsSearchCommand(queryParts, opts);
84+
});
85+
7786
plugins
7887
.command("inspect")
7988
.alias("info")
@@ -162,172 +171,8 @@ export function registerPluginsCli(program: Command) {
162171
.option("--force", "Skip confirmation prompt", false)
163172
.option("--dry-run", "Show what would be removed without making changes", false)
164173
.action(async (id: string, opts: PluginUninstallOptions) => {
165-
const {
166-
loadInstalledPluginIndexInstallRecords,
167-
removePluginInstallRecordFromRecords,
168-
withoutPluginInstallRecords,
169-
withPluginInstallRecords,
170-
} = await import("../plugins/installed-plugin-index-records.js");
171-
const { buildPluginSnapshotReport } = await import("../plugins/status.js");
172-
const {
173-
applyPluginUninstallDirectoryRemoval,
174-
formatUninstallActionLabels,
175-
formatUninstallSlotResetPreview,
176-
planPluginUninstall,
177-
resolveUninstallChannelConfigKeys,
178-
UNINSTALL_ACTION_LABELS,
179-
} = await import("../plugins/uninstall.js");
180-
const { commitPluginInstallRecordsWithConfig } =
181-
await import("./plugins-install-record-commit.js");
182-
const { refreshPluginRegistryAfterConfigMutation } =
183-
await import("./plugins-registry-refresh.js");
184-
const { resolvePluginUninstallId } = await import("./plugins-uninstall-selection.js");
185-
const { promptYesNo } = await import("./prompt.js");
186-
const snapshot = await tracePluginLifecyclePhaseAsync(
187-
"config read",
188-
() => readConfigFileSnapshot(),
189-
{ command: "uninstall" },
190-
);
191-
const sourceConfig = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
192-
const installRecords = await tracePluginLifecyclePhaseAsync(
193-
"install records load",
194-
() => loadInstalledPluginIndexInstallRecords(),
195-
{ command: "uninstall" },
196-
);
197-
const cfg = withPluginInstallRecords(sourceConfig, installRecords);
198-
const report = tracePluginLifecyclePhase(
199-
"plugin registry snapshot",
200-
() => buildPluginSnapshotReport({ config: cfg }),
201-
{ command: "uninstall" },
202-
);
203-
const extensionsDir = path.join(resolveStateDir(process.env, os.homedir), "extensions");
204-
const keepFiles = Boolean(opts.keepFiles || opts.keepConfig);
205-
206-
if (opts.keepConfig) {
207-
defaultRuntime.log(theme.warn("`--keep-config` is deprecated, use `--keep-files`."));
208-
}
209-
210-
const { plugin, pluginId } = resolvePluginUninstallId({
211-
rawId: id,
212-
config: cfg,
213-
plugins: report.plugins,
214-
});
215-
const hasEntry = pluginId in (cfg.plugins?.entries ?? {});
216-
const hasInstall = pluginId in (cfg.plugins?.installs ?? {});
217-
218-
if (!hasEntry && !hasInstall) {
219-
if (plugin) {
220-
defaultRuntime.error(
221-
`Plugin "${pluginId}" is not managed by plugins config/install records and cannot be uninstalled.`,
222-
);
223-
} else {
224-
defaultRuntime.error(`Plugin not found: ${id}`);
225-
}
226-
return defaultRuntime.exit(1);
227-
}
228-
229-
const channelIds = plugin?.status === "loaded" ? plugin.channelIds : undefined;
230-
const plan = planPluginUninstall({
231-
config: cfg,
232-
pluginId,
233-
channelIds,
234-
deleteFiles: !keepFiles,
235-
extensionsDir,
236-
});
237-
if (!plan.ok) {
238-
defaultRuntime.error(plan.error);
239-
return defaultRuntime.exit(1);
240-
}
241-
242-
const preview: string[] = [];
243-
if (plan.actions.entry) {
244-
preview.push(UNINSTALL_ACTION_LABELS.entry);
245-
}
246-
if (plan.actions.install) {
247-
preview.push(UNINSTALL_ACTION_LABELS.install);
248-
}
249-
if (plan.actions.allowlist) {
250-
preview.push(UNINSTALL_ACTION_LABELS.allowlist);
251-
}
252-
if (plan.actions.denylist) {
253-
preview.push(UNINSTALL_ACTION_LABELS.denylist);
254-
}
255-
if (plan.actions.loadPath) {
256-
preview.push(UNINSTALL_ACTION_LABELS.loadPath);
257-
}
258-
if (plan.actions.memorySlot) {
259-
preview.push(formatUninstallSlotResetPreview("memory"));
260-
}
261-
if (plan.actions.contextEngineSlot) {
262-
preview.push(formatUninstallSlotResetPreview("contextEngine"));
263-
}
264-
const channels = cfg.channels as Record<string, unknown> | undefined;
265-
if (plan.actions.channelConfig && hasInstall && channels) {
266-
for (const key of resolveUninstallChannelConfigKeys(pluginId, { channelIds })) {
267-
if (Object.hasOwn(channels, key)) {
268-
preview.push(`${UNINSTALL_ACTION_LABELS.channelConfig} (channels.${key})`);
269-
}
270-
}
271-
}
272-
if (plan.directoryRemoval) {
273-
preview.push(`directory: ${shortenHomePath(plan.directoryRemoval.target)}`);
274-
}
275-
276-
const pluginName = plugin?.name || pluginId;
277-
defaultRuntime.log(
278-
`Plugin: ${theme.command(pluginName)}${pluginName !== pluginId ? theme.muted(` (${pluginId})`) : ""}`,
279-
);
280-
defaultRuntime.log(`Will remove: ${preview.length > 0 ? preview.join(", ") : "(nothing)"}`);
281-
282-
if (opts.dryRun) {
283-
defaultRuntime.log(theme.muted("Dry run, no changes made."));
284-
return;
285-
}
286-
287-
if (!opts.force) {
288-
const confirmed = await promptYesNo(`Uninstall plugin "${pluginId}"?`);
289-
if (!confirmed) {
290-
defaultRuntime.log("Cancelled.");
291-
return;
292-
}
293-
}
294-
295-
const nextInstallRecords = removePluginInstallRecordFromRecords(installRecords, pluginId);
296-
const nextConfig = withoutPluginInstallRecords(plan.config);
297-
await tracePluginLifecyclePhaseAsync(
298-
"config mutation",
299-
() =>
300-
commitPluginInstallRecordsWithConfig({
301-
previousInstallRecords: installRecords,
302-
nextInstallRecords,
303-
nextConfig,
304-
...(snapshot.hash !== undefined ? { baseHash: snapshot.hash } : {}),
305-
}),
306-
{ command: "uninstall" },
307-
);
308-
const directoryResult = await applyPluginUninstallDirectoryRemoval(plan.directoryRemoval);
309-
for (const warning of directoryResult.warnings) {
310-
defaultRuntime.log(theme.warn(warning));
311-
}
312-
await refreshPluginRegistryAfterConfigMutation({
313-
config: nextConfig,
314-
reason: "source-changed",
315-
installRecords: nextInstallRecords,
316-
traceCommand: "uninstall",
317-
logger: {
318-
warn: (message) => defaultRuntime.log(theme.warn(message)),
319-
},
320-
});
321-
322-
const removed = formatUninstallActionLabels({
323-
...plan.actions,
324-
directory: directoryResult.directoryRemoved,
325-
});
326-
327-
defaultRuntime.log(
328-
`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`,
329-
);
330-
defaultRuntime.log("Restart the gateway to apply changes.");
174+
const { runPluginUninstallCommand } = await import("./plugins-uninstall-command.js");
175+
await runPluginUninstallCommand(id, opts);
331176
});
332177

333178
plugins

0 commit comments

Comments
 (0)