Skip to content

Commit e3cba91

Browse files
committed
fix(plugins): respect manifest optional tool siblings
1 parent a8b38bb commit e3cba91

3 files changed

Lines changed: 89 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
6868
- 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.
6969
- 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.
7070
- 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.
71+
- 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.
7172
- 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.
7273
- 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.
7374
- 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.

src/plugins/tools.optional.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,62 @@ describe("resolvePluginTools optional tools", () => {
12141214
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
12151215
});
12161216

1217+
it("does not materialize manifest-optional sibling tools from non-optional factories by default", async () => {
1218+
const config = createContext().config;
1219+
installToolManifestSnapshot({
1220+
config,
1221+
plugin: {
1222+
id: "multi",
1223+
origin: "bundled",
1224+
enabledByDefault: true,
1225+
channels: [],
1226+
providers: [],
1227+
contracts: {
1228+
tools: ["other_tool", "optional_tool"],
1229+
},
1230+
toolMetadata: {
1231+
optional_tool: {
1232+
optional: true,
1233+
},
1234+
},
1235+
},
1236+
});
1237+
const factory = vi.fn(() => [makeTool("other_tool"), makeTool("optional_tool")]);
1238+
setActivePluginRegistry(
1239+
createToolRegistry([
1240+
{
1241+
pluginId: "multi",
1242+
optional: false,
1243+
source: "/tmp/multi.js",
1244+
names: ["other_tool", "optional_tool"],
1245+
declaredNames: ["other_tool", "optional_tool"],
1246+
factory,
1247+
},
1248+
]) as never,
1249+
"test-tool-registry",
1250+
"gateway-bindable",
1251+
"/tmp",
1252+
);
1253+
const { loadManifestContractSnapshot } = await import("./manifest-contract-eligibility.js");
1254+
const snapshot = loadManifestContractSnapshot({ config, workspaceDir: "/tmp" });
1255+
expect(
1256+
snapshot.plugins.find((plugin) => plugin.id === "multi")?.toolMetadata?.optional_tool,
1257+
).toMatchObject({ optional: true });
1258+
1259+
const tools = resolvePluginTools(
1260+
createResolveToolsParams({
1261+
context: {
1262+
...createContext(),
1263+
config,
1264+
},
1265+
}),
1266+
);
1267+
1268+
expectResolvedToolNames(tools, ["other_tool"]);
1269+
expect(factory).toHaveBeenCalledTimes(1);
1270+
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
1271+
});
1272+
12171273
it("rejects plugin id collisions with core tool names", () => {
12181274
const registry = setRegistry([
12191275
{

src/plugins/tools.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,16 +1053,43 @@ export function resolvePluginTools(params: {
10531053
continue;
10541054
}
10551055
const listRaw: unknown[] = Array.isArray(resolved) ? resolved : [resolved];
1056+
const selectedManifestToolNames =
1057+
manifestPlugin && availabilityNames.length > 0
1058+
? new Set(allowlistNames.map((name) => normalizeToolName(name)))
1059+
: undefined;
1060+
const manifestContractToolNames =
1061+
manifestPlugin && availabilityNames.length > 0
1062+
? new Set(availabilityNames.map((name) => normalizeToolName(name)))
1063+
: undefined;
10561064
const availableList = manifestPlugin
1057-
? listRaw.filter((tool) =>
1058-
isManifestToolNameAvailable({
1065+
? listRaw.filter((tool) => {
1066+
const toolName = readPluginToolName(tool);
1067+
const normalizedToolName = normalizeToolName(toolName);
1068+
if (
1069+
isManifestToolOptional(manifestPlugin, toolName) &&
1070+
!isOptionalToolAllowed({
1071+
toolName,
1072+
pluginId: entry.pluginId,
1073+
allowlist,
1074+
})
1075+
) {
1076+
return false;
1077+
}
1078+
if (
1079+
selectedManifestToolNames &&
1080+
manifestContractToolNames?.has(normalizedToolName) &&
1081+
!selectedManifestToolNames.has(normalizedToolName)
1082+
) {
1083+
return false;
1084+
}
1085+
return isManifestToolNameAvailable({
10591086
plugin: manifestPlugin,
1060-
toolName: readPluginToolName(tool),
1087+
toolName,
10611088
config: params.context.runtimeConfig ?? context.config,
10621089
env,
10631090
hasAuthForProvider: params.hasAuthForProvider,
1064-
}),
1065-
)
1091+
});
1092+
})
10661093
: listRaw;
10671094
const policyAvailableList = availableList.filter(
10681095
(tool) =>

0 commit comments

Comments
 (0)