mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
fix(plugins): respect manifest optional tool siblings
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user