



















@@ -0,0 +1,196 @@
1+import { normalizeToolName } from "../../../agents/tool-policy-shared.js";
2+import type { OpenClawConfig } from "../../../config/types.openclaw.js";
3+import { normalizePluginId } from "../../../plugins/config-state.js";
4+import type { PluginManifestRegistry } from "../../../plugins/manifest-registry.js";
5+import { loadPluginManifestRegistryForPluginRegistry } from "../../../plugins/plugin-registry.js";
6+7+type ToolAllowlistSource = {
8+label: string;
9+entries: string[];
10+};
11+12+function hasRecord(value: unknown): value is Record<string, unknown> {
13+return Boolean(value && typeof value === "object" && !Array.isArray(value));
14+}
15+16+function normalizePluginIdMaybe(value: unknown): string | undefined {
17+return typeof value === "string" && value.trim() ? normalizePluginId(value) : undefined;
18+}
19+20+function collectListSource(params: { out: ToolAllowlistSource[]; value: unknown; label: string }) {
21+if (!Array.isArray(params.value)) {
22+return;
23+}
24+const entries = params.value
25+.filter((entry): entry is string => typeof entry === "string")
26+.map((entry) => entry.trim())
27+.filter(Boolean);
28+if (entries.length > 0) {
29+params.out.push({ label: params.label, entries });
30+}
31+}
32+33+function collectToolPolicySources(policy: unknown, label: string, out: ToolAllowlistSource[]) {
34+if (!hasRecord(policy)) {
35+return;
36+}
37+collectListSource({ out, value: policy.allow, label: `${label}.allow` });
38+collectListSource({ out, value: policy.alsoAllow, label: `${label}.alsoAllow` });
39+40+if (hasRecord(policy.byProvider)) {
41+for (const [providerId, providerPolicy] of Object.entries(policy.byProvider)) {
42+collectToolPolicySources(providerPolicy, `${label}.byProvider.${providerId}`, out);
43+}
44+}
45+46+const sandboxTools = hasRecord(policy.sandbox) ? policy.sandbox.tools : undefined;
47+collectToolPolicySources(sandboxTools, `${label}.sandbox.tools`, out);
48+49+const subagentTools = hasRecord(policy.subagents) ? policy.subagents.tools : undefined;
50+collectToolPolicySources(subagentTools, `${label}.subagents.tools`, out);
51+}
52+53+function collectToolAllowlistSources(cfg: OpenClawConfig): ToolAllowlistSource[] {
54+const sources: ToolAllowlistSource[] = [];
55+collectToolPolicySources(cfg.tools, "tools", sources);
56+const agentList = cfg.agents?.list;
57+if (Array.isArray(agentList)) {
58+agentList.forEach((agent, index) => {
59+if (!hasRecord(agent)) {
60+return;
61+}
62+collectToolPolicySources(agent.tools, `agents.list[${index}].tools`, sources);
63+});
64+}
65+return sources;
66+}
67+68+function formatSourceLabels(labels: Iterable<string>): string {
69+const sorted = [...new Set(labels)].toSorted((left, right) => left.localeCompare(right));
70+if (sorted.length <= 3) {
71+return sorted.join(", ");
72+}
73+return `${sorted.slice(0, 3).join(", ")} (+${sorted.length - 3} more)`;
74+}
75+76+function collectToolOwners(registry: PluginManifestRegistry): Map<string, string[]> {
77+const owners = new Map<string, string[]>();
78+for (const plugin of registry.plugins) {
79+const pluginId = normalizePluginId(plugin.id);
80+for (const toolNameRaw of plugin.contracts?.tools ?? []) {
81+const toolName = normalizeToolName(toolNameRaw);
82+if (!toolName) {
83+continue;
84+}
85+owners.set(toolName, [...(owners.get(toolName) ?? []), pluginId]);
86+}
87+}
88+return owners;
89+}
90+91+function collectKnownPluginIds(registry: PluginManifestRegistry): Set<string> {
92+return new Set(registry.plugins.map((plugin) => normalizePluginId(plugin.id)));
93+}
94+95+function formatPluginList(pluginIds: readonly string[]): string {
96+if (pluginIds.length === 1) {
97+return `"${pluginIds[0]}"`;
98+}
99+return pluginIds.map((pluginId) => `"${pluginId}"`).join(", ");
100+}
101+102+function addIssue(issues: Map<string, Set<string>>, key: string, sourceLabel: string) {
103+const sources = issues.get(key) ?? new Set<string>();
104+sources.add(sourceLabel);
105+issues.set(key, sources);
106+}
107+108+export function collectPluginToolAllowlistWarnings(params: {
109+cfg: OpenClawConfig;
110+env?: NodeJS.ProcessEnv;
111+manifestRegistry?: PluginManifestRegistry;
112+}): string[] {
113+if (params.cfg.plugins?.enabled === false) {
114+return [];
115+}
116+const allowedPluginIds = (params.cfg.plugins?.allow ?? [])
117+.map(normalizePluginIdMaybe)
118+.filter((pluginId): pluginId is string => Boolean(pluginId));
119+const allowedPlugins = new Set(allowedPluginIds);
120+if (allowedPlugins.size === 0) {
121+return [];
122+}
123+124+const sources = collectToolAllowlistSources(params.cfg);
125+if (sources.length === 0) {
126+return [];
127+}
128+129+const wildcardSources = sources
130+.filter((source) => source.entries.some((entry) => normalizeToolName(entry) === "*"))
131+.map((source) => source.label);
132+const warnings: string[] = [];
133+if (wildcardSources.length > 0) {
134+warnings.push(
135+`- plugins.allow is an exclusive plugin allowlist. ${formatSourceLabels(wildcardSources)} contains "*", but that wildcard only matches tools from plugins that are loaded; plugin tools outside plugins.allow stay unavailable. Add the required plugin ids to plugins.allow or remove plugins.allow.`,
136+);
137+}
138+139+const exactEntries = sources.flatMap((source) =>
140+source.entries
141+.map((entry) => ({ source: source.label, entry: normalizeToolName(entry) }))
142+.filter(({ entry }) => entry && entry !== "*" && entry !== "group:plugins"),
143+);
144+if (exactEntries.length === 0) {
145+return warnings;
146+}
147+148+const registry =
149+params.manifestRegistry ??
150+loadPluginManifestRegistryForPluginRegistry({
151+config: params.cfg,
152+env: params.env,
153+includeDisabled: true,
154+});
155+const knownPluginIds = collectKnownPluginIds(registry);
156+const toolOwners = collectToolOwners(registry);
157+const missingPluginIssues = new Map<string, Set<string>>();
158+const missingToolOwnerIssues = new Map<string, Set<string>>();
159+160+for (const { source, entry } of exactEntries) {
161+const pluginId = normalizePluginId(entry);
162+if (knownPluginIds.has(pluginId) && !allowedPlugins.has(pluginId)) {
163+addIssue(missingPluginIssues, pluginId, source);
164+continue;
165+}
166+167+const owners = (toolOwners.get(entry) ?? []).filter(
168+(ownerPluginId) => !allowedPlugins.has(ownerPluginId),
169+);
170+if (owners.length > 0 && owners.length === (toolOwners.get(entry) ?? []).length) {
171+addIssue(missingToolOwnerIssues, `${entry}\u0000${owners.join("\u0000")}`, source);
172+}
173+}
174+175+for (const [pluginId, issueSources] of [...missingPluginIssues.entries()].toSorted(
176+(left, right) => left[0].localeCompare(right[0]),
177+)) {
178+warnings.push(
179+`- ${formatSourceLabels(issueSources)} references plugin "${pluginId}", but plugins.allow does not include it. Add "${pluginId}" to plugins.allow or remove plugins.allow.`,
180+);
181+}
182+183+for (const [issueKey, issueSources] of [...missingToolOwnerIssues.entries()].toSorted(
184+(left, right) => left[0].localeCompare(right[0]),
185+)) {
186+const [toolName, ...ownerPluginIds] = issueKey.split("\u0000");
187+if (!toolName) {
188+continue;
189+}
190+warnings.push(
191+`- ${formatSourceLabels(issueSources)} references tool "${toolName}", owned by plugin ${formatPluginList(ownerPluginIds)}, but plugins.allow does not include the owning plugin. Add ${formatPluginList(ownerPluginIds)} to plugins.allow or remove plugins.allow.`,
192+);
193+}
194+195+return warnings;
196+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。