fix(plugins): respect manifest optional tool siblings

This commit is contained in:
Vincent Koc
2026-05-03 20:41:51 -07:00
parent a8b38bb742
commit e3cba91ef0
3 changed files with 89 additions and 5 deletions

View File

@@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
- Agents/tools: honor the effective tool denylist before constructing optional PDF/media tool factories, so `tools.deny: ["pdf"]` skips PDF setup before later policy filtering. Fixes #76997.
- MCP/plugin tools: apply global `tools.profile`, `tools.alsoAllow`, and `tools.deny` policy while exposing plugin tools over the standalone MCP bridge, so ACP clients do not see policy-hidden plugin tools or miss opt-in optional tools. Thanks @vincentkoc.
- Plugin tools: honor explicit tool denylists while selecting plugin tool runtimes, so denied plugin tools are not materialized for direct command or gateway surfaces before later policy filtering. Thanks @vincentkoc.
- Plugin tools: filter factory-returned tools by manifest per-tool optional policy, so optional sibling tools from a shared runtime factory stay hidden unless explicitly allowed. Thanks @vincentkoc.
- Agents/bootstrap: keep pending `BOOTSTRAP.md` and bootstrap truncation notices in system-prompt Project Context instead of copying setup text or raw warning diagnostics into WebChat user/runtime context. Fixes #76946.
- Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc.
- Gateway/install: keep `.env`-managed values in the macOS LaunchAgent env file while still tracking `OPENCLAW_SERVICE_MANAGED_ENV_KEYS`, so regenerated services do not boot without managed auth/provider keys. Fixes #75374.

View File

@@ -1214,6 +1214,62 @@ describe("resolvePluginTools optional tools", () => {
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("does not materialize manifest-optional sibling tools from non-optional factories by default", async () => {
const config = createContext().config;
installToolManifestSnapshot({
config,
plugin: {
id: "multi",
origin: "bundled",
enabledByDefault: true,
channels: [],
providers: [],
contracts: {
tools: ["other_tool", "optional_tool"],
},
toolMetadata: {
optional_tool: {
optional: true,
},
},
},
});
const factory = vi.fn(() => [makeTool("other_tool"), makeTool("optional_tool")]);
setActivePluginRegistry(
createToolRegistry([
{
pluginId: "multi",
optional: false,
source: "/tmp/multi.js",
names: ["other_tool", "optional_tool"],
declaredNames: ["other_tool", "optional_tool"],
factory,
},
]) as never,
"test-tool-registry",
"gateway-bindable",
"/tmp",
);
const { loadManifestContractSnapshot } = await import("./manifest-contract-eligibility.js");
const snapshot = loadManifestContractSnapshot({ config, workspaceDir: "/tmp" });
expect(
snapshot.plugins.find((plugin) => plugin.id === "multi")?.toolMetadata?.optional_tool,
).toMatchObject({ optional: true });
const tools = resolvePluginTools(
createResolveToolsParams({
context: {
...createContext(),
config,
},
}),
);
expectResolvedToolNames(tools, ["other_tool"]);
expect(factory).toHaveBeenCalledTimes(1);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("rejects plugin id collisions with core tool names", () => {
const registry = setRegistry([
{

View File

@@ -1053,16 +1053,43 @@ export function resolvePluginTools(params: {
continue;
}
const listRaw: unknown[] = Array.isArray(resolved) ? resolved : [resolved];
const selectedManifestToolNames =
manifestPlugin && availabilityNames.length > 0
? new Set(allowlistNames.map((name) => normalizeToolName(name)))
: undefined;
const manifestContractToolNames =
manifestPlugin && availabilityNames.length > 0
? new Set(availabilityNames.map((name) => normalizeToolName(name)))
: undefined;
const availableList = manifestPlugin
? listRaw.filter((tool) =>
isManifestToolNameAvailable({
? listRaw.filter((tool) => {
const toolName = readPluginToolName(tool);
const normalizedToolName = normalizeToolName(toolName);
if (
isManifestToolOptional(manifestPlugin, toolName) &&
!isOptionalToolAllowed({
toolName,
pluginId: entry.pluginId,
allowlist,
})
) {
return false;
}
if (
selectedManifestToolNames &&
manifestContractToolNames?.has(normalizedToolName) &&
!selectedManifestToolNames.has(normalizedToolName)
) {
return false;
}
return isManifestToolNameAvailable({
plugin: manifestPlugin,
toolName: readPluginToolName(tool),
toolName,
config: params.context.runtimeConfig ?? context.config,
env,
hasAuthForProvider: params.hasAuthForProvider,
}),
)
});
})
: listRaw;
const policyAvailableList = availableList.filter(
(tool) =>