Plugins: add inspect command and capability report
This commit is contained in:
parent
e4825a0f93
commit
3983928958
@ -35,7 +35,7 @@ describe("handleCommands /plugins", () => {
|
||||
await workspaceHarness.cleanupWorkspaces();
|
||||
});
|
||||
|
||||
it("lists discovered plugins and shows plugin details", async () => {
|
||||
it("lists discovered plugins and inspects plugin details", async () => {
|
||||
await withTempHome("openclaw-command-plugins-home-", async () => {
|
||||
const workspaceDir = await workspaceHarness.createWorkspace();
|
||||
await createClaudeBundlePlugin({ workspaceDir, pluginId: "superpowers" });
|
||||
@ -49,13 +49,19 @@ describe("handleCommands /plugins", () => {
|
||||
expect(listResult.reply?.text).toContain("superpowers");
|
||||
expect(listResult.reply?.text).toContain("[disabled]");
|
||||
|
||||
const showParams = buildCommandTestParams("/plugin show superpowers", buildCfg(), undefined, {
|
||||
workspaceDir,
|
||||
});
|
||||
const showParams = buildCommandTestParams(
|
||||
"/plugins inspect superpowers",
|
||||
buildCfg(),
|
||||
undefined,
|
||||
{
|
||||
workspaceDir,
|
||||
},
|
||||
);
|
||||
showParams.command.senderIsOwner = true;
|
||||
const showResult = await handleCommands(showParams);
|
||||
expect(showResult.reply?.text).toContain('"id": "superpowers"');
|
||||
expect(showResult.reply?.text).toContain('"bundleFormat": "claude"');
|
||||
expect(showResult.reply?.text).toContain('"shape":');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -4,8 +4,13 @@ import {
|
||||
writeConfigFile,
|
||||
} from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { PluginInstallRecord } from "../../config/types.plugins.js";
|
||||
import type { PluginRecord } from "../../plugins/registry.js";
|
||||
import { buildPluginStatusReport, type PluginStatusReport } from "../../plugins/status.js";
|
||||
import {
|
||||
buildPluginInspectReport,
|
||||
buildPluginStatusReport,
|
||||
type PluginStatusReport,
|
||||
} from "../../plugins/status.js";
|
||||
import { setPluginEnabledInConfig } from "../../plugins/toggle-config.js";
|
||||
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
@ -21,6 +26,28 @@ function renderJsonBlock(label: string, value: unknown): string {
|
||||
return `${label}\n\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
||||
}
|
||||
|
||||
function buildPluginInspectJson(params: {
|
||||
id: string;
|
||||
config: OpenClawConfig;
|
||||
report: PluginStatusReport;
|
||||
}): {
|
||||
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>;
|
||||
install: PluginInstallRecord | null;
|
||||
} | null {
|
||||
const inspect = buildPluginInspectReport({
|
||||
id: params.id,
|
||||
config: params.config,
|
||||
report: params.report,
|
||||
});
|
||||
if (!inspect) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
inspect,
|
||||
install: params.config.plugins?.installs?.[inspect.plugin.id] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function formatPluginLabel(plugin: PluginRecord): string {
|
||||
if (!plugin.name || plugin.name === plugin.id) {
|
||||
return plugin.id;
|
||||
@ -95,7 +122,7 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm
|
||||
return unauthorized;
|
||||
}
|
||||
const allowInternalReadOnly =
|
||||
(pluginsCommand.action === "list" || pluginsCommand.action === "show") &&
|
||||
(pluginsCommand.action === "list" || pluginsCommand.action === "inspect") &&
|
||||
isInternalMessageChannel(params.command.channel);
|
||||
const nonOwner = allowInternalReadOnly ? null : rejectNonOwnerCommand(params, "/plugins");
|
||||
if (nonOwner) {
|
||||
@ -130,27 +157,30 @@ export const handlePluginsCommand: CommandHandler = async (params, allowTextComm
|
||||
};
|
||||
}
|
||||
|
||||
if (pluginsCommand.action === "show") {
|
||||
if (pluginsCommand.action === "inspect") {
|
||||
if (!pluginsCommand.name) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: formatPluginsList(loaded.report) },
|
||||
};
|
||||
}
|
||||
const plugin = findPlugin(loaded.report, pluginsCommand.name);
|
||||
if (!plugin) {
|
||||
const payload = buildPluginInspectJson({
|
||||
id: pluginsCommand.name,
|
||||
config: loaded.config,
|
||||
report: loaded.report,
|
||||
});
|
||||
if (!payload) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: `🔌 No plugin named "${pluginsCommand.name}" found.` },
|
||||
};
|
||||
}
|
||||
const install = loaded.config.plugins?.installs?.[plugin.id] ?? null;
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: renderJsonBlock(`🔌 Plugin "${plugin.id}"`, {
|
||||
plugin,
|
||||
install,
|
||||
text: renderJsonBlock(`🔌 Plugin "${payload.inspect.plugin.id}"`, {
|
||||
...payload.inspect,
|
||||
install: payload.install,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export type PluginsCommand =
|
||||
| { action: "list" }
|
||||
| { action: "show"; name?: string }
|
||||
| { action: "inspect"; name?: string }
|
||||
| { action: "enable"; name: string }
|
||||
| { action: "disable"; name: string }
|
||||
| { action: "error"; message: string };
|
||||
@ -22,12 +22,15 @@ export function parsePluginsCommand(raw: string): PluginsCommand | null {
|
||||
|
||||
if (action === "list") {
|
||||
return name
|
||||
? { action: "error", message: "Usage: /plugins list|show|get|enable|disable [plugin]" }
|
||||
? {
|
||||
action: "error",
|
||||
message: "Usage: /plugins list|inspect|show|get|enable|disable [plugin]",
|
||||
}
|
||||
: { action: "list" };
|
||||
}
|
||||
|
||||
if (action === "show" || action === "get") {
|
||||
return { action: "show", name: name || undefined };
|
||||
if (action === "inspect" || action === "show" || action === "get") {
|
||||
return { action: "inspect", name: name || undefined };
|
||||
}
|
||||
|
||||
if (action === "enable" || action === "disable") {
|
||||
@ -42,6 +45,6 @@ export function parsePluginsCommand(raw: string): PluginsCommand | null {
|
||||
|
||||
return {
|
||||
action: "error",
|
||||
message: "Usage: /plugins list|show|get|enable|disable [plugin]",
|
||||
message: "Usage: /plugins list|inspect|show|get|enable|disable [plugin]",
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import type { Command } from "commander";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadConfig, writeConfigFile } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { resolveArchiveKind } from "../infra/archive.js";
|
||||
import { type BundledPluginSource, findBundledPluginSource } from "../plugins/bundled-sources.js";
|
||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||
@ -19,7 +20,7 @@ import {
|
||||
import type { PluginRecord } from "../plugins/registry.js";
|
||||
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
|
||||
import { resolvePluginSourceRoots, formatPluginSourceForTable } from "../plugins/source-display.js";
|
||||
import { buildPluginStatusReport } from "../plugins/status.js";
|
||||
import { buildPluginInspectReport, buildPluginStatusReport } from "../plugins/status.js";
|
||||
import { resolveUninstallDirectoryTarget, uninstallPlugin } from "../plugins/uninstall.js";
|
||||
import { updateNpmInstalledPlugins } from "../plugins/update.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@ -42,7 +43,7 @@ export type PluginsListOptions = {
|
||||
verbose?: boolean;
|
||||
};
|
||||
|
||||
export type PluginInfoOptions = {
|
||||
export type PluginInspectOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
@ -133,6 +134,36 @@ function formatPluginLine(plugin: PluginRecord, verbose = false): string {
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function formatInspectSection(title: string, lines: string[]): string[] {
|
||||
if (lines.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return ["", `${theme.muted(`${title}:`)}`, ...lines];
|
||||
}
|
||||
|
||||
function formatInstallLines(install: PluginInstallRecord | undefined): string[] {
|
||||
if (!install) {
|
||||
return [];
|
||||
}
|
||||
const lines = [`Source: ${install.source}`];
|
||||
if (install.spec) {
|
||||
lines.push(`Spec: ${install.spec}`);
|
||||
}
|
||||
if (install.sourcePath) {
|
||||
lines.push(`Source path: ${shortenHomePath(install.sourcePath)}`);
|
||||
}
|
||||
if (install.installPath) {
|
||||
lines.push(`Install path: ${shortenHomePath(install.installPath)}`);
|
||||
}
|
||||
if (install.version) {
|
||||
lines.push(`Recorded version: ${install.version}`);
|
||||
}
|
||||
if (install.installedAt) {
|
||||
lines.push(`Installed at: ${install.installedAt}`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function applySlotSelectionForPlugin(
|
||||
config: OpenClawConfig,
|
||||
pluginId: string,
|
||||
@ -542,88 +573,133 @@ export function registerPluginsCli(program: Command) {
|
||||
});
|
||||
|
||||
plugins
|
||||
.command("info")
|
||||
.description("Show plugin details")
|
||||
.command("inspect")
|
||||
.alias("info")
|
||||
.description("Inspect plugin details")
|
||||
.argument("<id>", "Plugin id")
|
||||
.option("--json", "Print JSON")
|
||||
.action((id: string, opts: PluginInfoOptions) => {
|
||||
const report = buildPluginStatusReport();
|
||||
const plugin = report.plugins.find((p) => p.id === id || p.name === id);
|
||||
if (!plugin) {
|
||||
.action((id: string, opts: PluginInspectOptions) => {
|
||||
const cfg = loadConfig();
|
||||
const report = buildPluginStatusReport({ config: cfg });
|
||||
const inspect = buildPluginInspectReport({
|
||||
id,
|
||||
config: cfg,
|
||||
report,
|
||||
});
|
||||
if (!inspect) {
|
||||
defaultRuntime.error(`Plugin not found: ${id}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const install = cfg.plugins?.installs?.[plugin.id];
|
||||
const install = cfg.plugins?.installs?.[inspect.plugin.id];
|
||||
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(plugin, null, 2));
|
||||
defaultRuntime.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...inspect,
|
||||
install,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.heading(plugin.name || plugin.id));
|
||||
if (plugin.name && plugin.name !== plugin.id) {
|
||||
lines.push(theme.muted(`id: ${plugin.id}`));
|
||||
lines.push(theme.heading(inspect.plugin.name || inspect.plugin.id));
|
||||
if (inspect.plugin.name && inspect.plugin.name !== inspect.plugin.id) {
|
||||
lines.push(theme.muted(`id: ${inspect.plugin.id}`));
|
||||
}
|
||||
if (plugin.description) {
|
||||
lines.push(plugin.description);
|
||||
if (inspect.plugin.description) {
|
||||
lines.push(inspect.plugin.description);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(`${theme.muted("Status:")} ${plugin.status}`);
|
||||
lines.push(`${theme.muted("Format:")} ${plugin.format ?? "openclaw"}`);
|
||||
if (plugin.bundleFormat) {
|
||||
lines.push(`${theme.muted("Bundle format:")} ${plugin.bundleFormat}`);
|
||||
lines.push(`${theme.muted("Status:")} ${inspect.plugin.status}`);
|
||||
lines.push(`${theme.muted("Format:")} ${inspect.plugin.format ?? "openclaw"}`);
|
||||
if (inspect.plugin.bundleFormat) {
|
||||
lines.push(`${theme.muted("Bundle format:")} ${inspect.plugin.bundleFormat}`);
|
||||
}
|
||||
lines.push(`${theme.muted("Source:")} ${shortenHomeInString(plugin.source)}`);
|
||||
lines.push(`${theme.muted("Origin:")} ${plugin.origin}`);
|
||||
if (plugin.version) {
|
||||
lines.push(`${theme.muted("Version:")} ${plugin.version}`);
|
||||
lines.push(`${theme.muted("Source:")} ${shortenHomeInString(inspect.plugin.source)}`);
|
||||
lines.push(`${theme.muted("Origin:")} ${inspect.plugin.origin}`);
|
||||
if (inspect.plugin.version) {
|
||||
lines.push(`${theme.muted("Version:")} ${inspect.plugin.version}`);
|
||||
}
|
||||
if (plugin.toolNames.length > 0) {
|
||||
lines.push(`${theme.muted("Tools:")} ${plugin.toolNames.join(", ")}`);
|
||||
}
|
||||
if (plugin.hookNames.length > 0) {
|
||||
lines.push(`${theme.muted("Hooks:")} ${plugin.hookNames.join(", ")}`);
|
||||
}
|
||||
if (plugin.gatewayMethods.length > 0) {
|
||||
lines.push(`${theme.muted("Gateway methods:")} ${plugin.gatewayMethods.join(", ")}`);
|
||||
}
|
||||
if (plugin.providerIds.length > 0) {
|
||||
lines.push(`${theme.muted("Providers:")} ${plugin.providerIds.join(", ")}`);
|
||||
}
|
||||
if ((plugin.bundleCapabilities?.length ?? 0) > 0) {
|
||||
lines.push(`${theme.muted("Shape:")} ${inspect.shape}`);
|
||||
lines.push(`${theme.muted("Capability mode:")} ${inspect.capabilityMode}`);
|
||||
lines.push(
|
||||
`${theme.muted("Legacy before_agent_start:")} ${inspect.usesLegacyBeforeAgentStart ? "yes" : "no"}`,
|
||||
);
|
||||
if ((inspect.plugin.bundleCapabilities?.length ?? 0) > 0) {
|
||||
lines.push(
|
||||
`${theme.muted("Bundle capabilities:")} ${plugin.bundleCapabilities?.join(", ")}`,
|
||||
`${theme.muted("Bundle capabilities:")} ${inspect.plugin.bundleCapabilities?.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (plugin.cliCommands.length > 0) {
|
||||
lines.push(`${theme.muted("CLI commands:")} ${plugin.cliCommands.join(", ")}`);
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Capabilities",
|
||||
inspect.capabilities.map(
|
||||
(entry) =>
|
||||
`${entry.kind}: ${entry.ids.length > 0 ? entry.ids.join(", ") : "(registered)"}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Typed hooks",
|
||||
inspect.typedHooks.map((entry) =>
|
||||
entry.priority == null ? entry.name : `${entry.name} (priority ${entry.priority})`,
|
||||
),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Custom hooks",
|
||||
inspect.customHooks.map((entry) => `${entry.name}: ${entry.events.join(", ")}`),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Tools",
|
||||
inspect.tools.map((entry) => {
|
||||
const names = entry.names.length > 0 ? entry.names.join(", ") : "(anonymous)";
|
||||
return entry.optional ? `${names} [optional]` : names;
|
||||
}),
|
||||
),
|
||||
);
|
||||
lines.push(...formatInspectSection("Commands", inspect.commands));
|
||||
lines.push(...formatInspectSection("CLI commands", inspect.cliCommands));
|
||||
lines.push(...formatInspectSection("Services", inspect.services));
|
||||
lines.push(...formatInspectSection("Gateway methods", inspect.gatewayMethods));
|
||||
if (inspect.httpRouteCount > 0) {
|
||||
lines.push(...formatInspectSection("HTTP routes", [String(inspect.httpRouteCount)]));
|
||||
}
|
||||
if (plugin.services.length > 0) {
|
||||
lines.push(`${theme.muted("Services:")} ${plugin.services.join(", ")}`);
|
||||
const policyLines: string[] = [];
|
||||
if (typeof inspect.policy.allowPromptInjection === "boolean") {
|
||||
policyLines.push(`allowPromptInjection: ${inspect.policy.allowPromptInjection}`);
|
||||
}
|
||||
if (plugin.error) {
|
||||
lines.push(`${theme.error("Error:")} ${plugin.error}`);
|
||||
if (typeof inspect.policy.allowModelOverride === "boolean") {
|
||||
policyLines.push(`allowModelOverride: ${inspect.policy.allowModelOverride}`);
|
||||
}
|
||||
if (install) {
|
||||
lines.push("");
|
||||
lines.push(`${theme.muted("Install:")} ${install.source}`);
|
||||
if (install.spec) {
|
||||
lines.push(`${theme.muted("Spec:")} ${install.spec}`);
|
||||
}
|
||||
if (install.sourcePath) {
|
||||
lines.push(`${theme.muted("Source path:")} ${shortenHomePath(install.sourcePath)}`);
|
||||
}
|
||||
if (install.installPath) {
|
||||
lines.push(`${theme.muted("Install path:")} ${shortenHomePath(install.installPath)}`);
|
||||
}
|
||||
if (install.version) {
|
||||
lines.push(`${theme.muted("Recorded version:")} ${install.version}`);
|
||||
}
|
||||
if (install.installedAt) {
|
||||
lines.push(`${theme.muted("Installed at:")} ${install.installedAt}`);
|
||||
}
|
||||
if (inspect.policy.hasAllowedModelsConfig) {
|
||||
policyLines.push(
|
||||
`allowedModels: ${
|
||||
inspect.policy.allowedModels.length > 0
|
||||
? inspect.policy.allowedModels.join(", ")
|
||||
: "(configured but empty)"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
lines.push(...formatInspectSection("Policy", policyLines));
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Diagnostics",
|
||||
inspect.diagnostics.map((entry) => `${entry.level.toUpperCase()}: ${entry.message}`),
|
||||
),
|
||||
);
|
||||
lines.push(...formatInspectSection("Install", formatInstallLines(install)));
|
||||
if (inspect.plugin.error) {
|
||||
lines.push("", `${theme.error("Error:")} ${inspect.plugin.error}`);
|
||||
}
|
||||
defaultRuntime.log(lines.join("\n"));
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const loadConfigMock = vi.fn();
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
let buildPluginStatusReport: typeof import("./status.js").buildPluginStatusReport;
|
||||
let buildPluginInspectReport: typeof import("./status.js").buildPluginInspectReport;
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: () => loadConfigMock(),
|
||||
@ -32,14 +33,21 @@ describe("buildPluginStatusReport", () => {
|
||||
diagnostics: [],
|
||||
channels: [],
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channelSetups: [],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
});
|
||||
({ buildPluginStatusReport } = await import("./status.js"));
|
||||
({ buildPluginInspectReport, buildPluginStatusReport } = await import("./status.js"));
|
||||
});
|
||||
|
||||
it("forwards an explicit env to plugin loading", () => {
|
||||
@ -59,4 +67,93 @@ describe("buildPluginStatusReport", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds an inspect report with capability shape and policy", () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
hooks: { allowPromptInjection: false },
|
||||
subagent: {
|
||||
allowModelOverride: true,
|
||||
allowedModels: ["openai/gpt-5.4"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "google",
|
||||
name: "Google",
|
||||
description: "Google provider plugin",
|
||||
source: "/tmp/google/index.ts",
|
||||
origin: "bundled",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: ["google"],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: ["google"],
|
||||
imageGenerationProviderIds: ["google"],
|
||||
webSearchProviderIds: ["google"],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
diagnostics: [{ level: "warn", pluginId: "google", message: "watch this seam" }],
|
||||
channels: [],
|
||||
channelSetups: [],
|
||||
providers: [],
|
||||
speechProviders: [],
|
||||
mediaUnderstandingProviders: [],
|
||||
imageGenerationProviders: [],
|
||||
webSearchProviders: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [
|
||||
{
|
||||
pluginId: "google",
|
||||
hookName: "before_agent_start",
|
||||
handler: () => undefined,
|
||||
source: "/tmp/google/index.ts",
|
||||
},
|
||||
],
|
||||
httpRoutes: [],
|
||||
gatewayHandlers: {},
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
});
|
||||
|
||||
const inspect = buildPluginInspectReport({ id: "google" });
|
||||
|
||||
expect(inspect).not.toBeNull();
|
||||
expect(inspect?.shape).toBe("hybrid-capability");
|
||||
expect(inspect?.capabilityMode).toBe("hybrid");
|
||||
expect(inspect?.capabilities.map((entry) => entry.kind)).toEqual([
|
||||
"text-inference",
|
||||
"media-understanding",
|
||||
"image-generation",
|
||||
"web-search",
|
||||
]);
|
||||
expect(inspect?.usesLegacyBeforeAgentStart).toBe(true);
|
||||
expect(inspect?.policy).toEqual({
|
||||
allowPromptInjection: false,
|
||||
allowModelOverride: true,
|
||||
allowedModels: ["openai/gpt-5.4"],
|
||||
hasAllowedModelsConfig: true,
|
||||
});
|
||||
expect(inspect?.diagnostics).toEqual([
|
||||
{ level: "warn", pluginId: "google", message: "watch this seam" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,14 +2,67 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizePluginsConfig } from "./config-state.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
import type { PluginDiagnostic, PluginHookName } from "./types.js";
|
||||
|
||||
export type PluginStatusReport = PluginRegistry & {
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
export type PluginCapabilityKind =
|
||||
| "text-inference"
|
||||
| "speech"
|
||||
| "media-understanding"
|
||||
| "image-generation"
|
||||
| "web-search"
|
||||
| "channel";
|
||||
|
||||
export type PluginInspectShape =
|
||||
| "hook-only"
|
||||
| "plain-capability"
|
||||
| "hybrid-capability"
|
||||
| "non-capability";
|
||||
|
||||
export type PluginInspectReport = {
|
||||
workspaceDir?: string;
|
||||
plugin: PluginRegistry["plugins"][number];
|
||||
shape: PluginInspectShape;
|
||||
capabilityMode: "none" | "plain" | "hybrid";
|
||||
capabilityCount: number;
|
||||
capabilities: Array<{
|
||||
kind: PluginCapabilityKind;
|
||||
ids: string[];
|
||||
}>;
|
||||
typedHooks: Array<{
|
||||
name: PluginHookName;
|
||||
priority?: number;
|
||||
}>;
|
||||
customHooks: Array<{
|
||||
name: string;
|
||||
events: string[];
|
||||
}>;
|
||||
tools: Array<{
|
||||
names: string[];
|
||||
optional: boolean;
|
||||
}>;
|
||||
commands: string[];
|
||||
cliCommands: string[];
|
||||
services: string[];
|
||||
gatewayMethods: string[];
|
||||
httpRouteCount: number;
|
||||
diagnostics: PluginDiagnostic[];
|
||||
policy: {
|
||||
allowPromptInjection?: boolean;
|
||||
allowModelOverride?: boolean;
|
||||
allowedModels: string[];
|
||||
hasAllowedModelsConfig: boolean;
|
||||
};
|
||||
usesLegacyBeforeAgentStart: boolean;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
export function buildPluginStatusReport(params?: {
|
||||
@ -36,3 +89,126 @@ export function buildPluginStatusReport(params?: {
|
||||
...registry,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCapabilityEntries(plugin: PluginRegistry["plugins"][number]) {
|
||||
return [
|
||||
{ kind: "text-inference" as const, ids: plugin.providerIds },
|
||||
{ kind: "speech" as const, ids: plugin.speechProviderIds },
|
||||
{ kind: "media-understanding" as const, ids: plugin.mediaUnderstandingProviderIds },
|
||||
{ kind: "image-generation" as const, ids: plugin.imageGenerationProviderIds },
|
||||
{ kind: "web-search" as const, ids: plugin.webSearchProviderIds },
|
||||
{ kind: "channel" as const, ids: plugin.channelIds },
|
||||
].filter((entry) => entry.ids.length > 0);
|
||||
}
|
||||
|
||||
function deriveInspectShape(params: {
|
||||
capabilityCount: number;
|
||||
typedHookCount: number;
|
||||
customHookCount: number;
|
||||
toolCount: number;
|
||||
commandCount: number;
|
||||
cliCount: number;
|
||||
serviceCount: number;
|
||||
gatewayMethodCount: number;
|
||||
httpRouteCount: number;
|
||||
}): PluginInspectShape {
|
||||
if (params.capabilityCount > 1) {
|
||||
return "hybrid-capability";
|
||||
}
|
||||
if (params.capabilityCount === 1) {
|
||||
return "plain-capability";
|
||||
}
|
||||
const hasOnlyHooks =
|
||||
params.typedHookCount + params.customHookCount > 0 &&
|
||||
params.toolCount === 0 &&
|
||||
params.commandCount === 0 &&
|
||||
params.cliCount === 0 &&
|
||||
params.serviceCount === 0 &&
|
||||
params.gatewayMethodCount === 0 &&
|
||||
params.httpRouteCount === 0;
|
||||
if (hasOnlyHooks) {
|
||||
return "hook-only";
|
||||
}
|
||||
return "non-capability";
|
||||
}
|
||||
|
||||
export function buildPluginInspectReport(params: {
|
||||
id: string;
|
||||
config?: ReturnType<typeof loadConfig>;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
report?: PluginStatusReport;
|
||||
}): PluginInspectReport | null {
|
||||
const config = params.config ?? loadConfig();
|
||||
const report =
|
||||
params.report ??
|
||||
buildPluginStatusReport({
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const plugin = report.plugins.find((entry) => entry.id === params.id || entry.name === params.id);
|
||||
if (!plugin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capabilities = buildCapabilityEntries(plugin);
|
||||
const typedHooks = report.typedHooks
|
||||
.filter((entry) => entry.pluginId === plugin.id)
|
||||
.map((entry) => ({
|
||||
name: entry.hookName,
|
||||
priority: entry.priority,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const customHooks = report.hooks
|
||||
.filter((entry) => entry.pluginId === plugin.id)
|
||||
.map((entry) => ({
|
||||
name: entry.entry.hook.name,
|
||||
events: [...entry.events].sort(),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const tools = report.tools
|
||||
.filter((entry) => entry.pluginId === plugin.id)
|
||||
.map((entry) => ({
|
||||
names: [...entry.names],
|
||||
optional: entry.optional,
|
||||
}));
|
||||
const diagnostics = report.diagnostics.filter((entry) => entry.pluginId === plugin.id);
|
||||
const policyEntry = normalizePluginsConfig(config.plugins).entries[plugin.id];
|
||||
const capabilityCount = capabilities.length;
|
||||
|
||||
return {
|
||||
workspaceDir: report.workspaceDir,
|
||||
plugin,
|
||||
shape: deriveInspectShape({
|
||||
capabilityCount,
|
||||
typedHookCount: typedHooks.length,
|
||||
customHookCount: customHooks.length,
|
||||
toolCount: tools.length,
|
||||
commandCount: plugin.commands.length,
|
||||
cliCount: plugin.cliCommands.length,
|
||||
serviceCount: plugin.services.length,
|
||||
gatewayMethodCount: plugin.gatewayMethods.length,
|
||||
httpRouteCount: plugin.httpRoutes,
|
||||
}),
|
||||
capabilityMode: capabilityCount === 0 ? "none" : capabilityCount === 1 ? "plain" : "hybrid",
|
||||
capabilityCount,
|
||||
capabilities,
|
||||
typedHooks,
|
||||
customHooks,
|
||||
tools,
|
||||
commands: [...plugin.commands],
|
||||
cliCommands: [...plugin.cliCommands],
|
||||
services: [...plugin.services],
|
||||
gatewayMethods: [...plugin.gatewayMethods],
|
||||
httpRouteCount: plugin.httpRoutes,
|
||||
diagnostics,
|
||||
policy: {
|
||||
allowPromptInjection: policyEntry?.hooks?.allowPromptInjection,
|
||||
allowModelOverride: policyEntry?.subagent?.allowModelOverride,
|
||||
allowedModels: [...(policyEntry?.subagent?.allowedModels ?? [])],
|
||||
hasAllowedModelsConfig: policyEntry?.subagent?.hasAllowedModelsConfig === true,
|
||||
},
|
||||
usesLegacyBeforeAgentStart: typedHooks.some((entry) => entry.name === "before_agent_start"),
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user