fix(agents): keep exact tool allowlists on owning factories (#104213)

* refactor(agents): centralize core tool factory descriptors

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Ayaan Zaidi
2026-07-11 12:44:37 +05:30
committed by GitHub
parent 34a8472274
commit f2a0f00567
5 changed files with 152 additions and 72 deletions
+1 -8
View File
@@ -64,6 +64,7 @@ import {
resolveConversationCapabilityProfile,
type ResolvedConversationCapabilityProfile,
} from "./conversation-capability-profile.js";
import type { OpenClawCodingToolConstructionPlan } from "./core-tool-factory-descriptors.js";
import { resolveImageSanitizationLimits } from "./image-sanitization.js";
import { createLazyExecTool, resolveExecToolConfig } from "./lazy-exec-tool.js";
import {
@@ -304,14 +305,6 @@ export const testing = {
applyModelProviderToolPolicy,
} as const;
export type OpenClawCodingToolConstructionPlan = {
includeBaseCodingTools: boolean;
includeShellTools: boolean;
includeChannelTools: boolean;
includeOpenClawTools: boolean;
includePluginTools: boolean;
};
/** Build the runtime tool list for one agent run. */
export function createOpenClawCodingTools(options?: {
agentId?: string;
@@ -0,0 +1,66 @@
/**
* Static identity for names that select core agent factory families before assembly.
*/
export type CoreToolFactoryFamily = "base-coding" | "shell" | "openclaw";
type CoreToolFactoryDescriptor = {
name: string;
family: CoreToolFactoryFamily;
};
export const CORE_TOOL_FACTORY_DESCRIPTORS = [
{ name: "edit", family: "base-coding" },
{ name: "read", family: "base-coding" },
{ name: "write", family: "base-coding" },
{ name: "apply_patch", family: "shell" },
{ name: "exec", family: "shell" },
{ name: "process", family: "shell" },
{ name: "agents_list", family: "openclaw" },
{ name: "crestodian", family: "openclaw" },
{ name: "computer", family: "openclaw" },
{ name: "cron", family: "openclaw" },
{ name: "gateway", family: "openclaw" },
{ name: "get_goal", family: "openclaw" },
{ name: "heartbeat_respond", family: "openclaw" },
{ name: "image", family: "openclaw" },
{ name: "image_generate", family: "openclaw" },
{ name: "message", family: "openclaw" },
{ name: "music_generate", family: "openclaw" },
{ name: "nodes", family: "openclaw" },
{ name: "pdf", family: "openclaw" },
{ name: "session_status", family: "openclaw" },
{ name: "sessions_history", family: "openclaw" },
{ name: "sessions_list", family: "openclaw" },
{ name: "sessions_send", family: "openclaw" },
{ name: "sessions_spawn", family: "openclaw" },
{ name: "sessions_yield", family: "openclaw" },
{ name: "skill_workshop", family: "openclaw" },
{ name: "spawn_task", family: "openclaw" },
{ name: "create_goal", family: "openclaw" },
{ name: "subagents", family: "openclaw" },
{ name: "transcripts", family: "openclaw" },
{ name: "tts", family: "openclaw" },
{ name: "update_goal", family: "openclaw" },
{ name: "update_plan", family: "openclaw" },
{ name: "dismiss_task", family: "openclaw" },
{ name: "video_generate", family: "openclaw" },
{ name: "web_fetch", family: "openclaw" },
{ name: "web_search", family: "openclaw" },
] as const satisfies readonly CoreToolFactoryDescriptor[];
const CORE_TOOL_FACTORY_FAMILY_BY_NAME = new Map<string, CoreToolFactoryFamily>(
CORE_TOOL_FACTORY_DESCRIPTORS.map(({ name, family }) => [name, family]),
);
export type OpenClawCodingToolConstructionPlan = {
includeBaseCodingTools: boolean;
includeShellTools: boolean;
includeChannelTools: boolean;
includeOpenClawTools: boolean;
includePluginTools: boolean;
};
export function resolveCoreToolFactoryFamily(name: string): CoreToolFactoryFamily | undefined {
return CORE_TOOL_FACTORY_FAMILY_BY_NAME.get(name);
}
@@ -1,5 +1,6 @@
// Coverage for embedded attempt tool construction and runtime allowlists.
import { describe, expect, it } from "vitest";
import { CORE_TOOL_FACTORY_DESCRIPTORS } from "../../core-tool-factory-descriptors.js";
import {
applyEmbeddedAttemptToolsAllow,
mergeForcedEmbeddedAttemptToolsAllow,
@@ -182,6 +183,26 @@ describe("applyEmbeddedAttemptToolsAllow", () => {
});
describe("resolveEmbeddedAttemptToolConstructionPlan", () => {
it("keeps factory descriptor names unique", () => {
const names = CORE_TOOL_FACTORY_DESCRIPTORS.map((descriptor) => descriptor.name);
expect(new Set(names).size).toBe(names.length);
});
it.each(CORE_TOOL_FACTORY_DESCRIPTORS)(
"routes $name through its $family factory",
({ name, family }) => {
const plan = resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: [name] });
expect(plan.codingToolConstructionPlan).toMatchObject({
includeBaseCodingTools: family === "base-coding",
includeShellTools: family === "shell",
includeOpenClawTools: family === "openclaw",
includePluginTools: false,
});
},
);
it("builds all tool families when no runtime allowlist is present", () => {
expectConstructionPlan(resolveEmbeddedAttemptToolConstructionPlan({}), {
constructTools: true,
@@ -386,7 +407,29 @@ describe("resolveEmbeddedAttemptToolConstructionPlan", () => {
);
});
it("materializes transcripts through the core factory", () => {
expectConstructionPlan(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["transcripts"] }),
{
includeCoreTools: true,
coding: {
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
},
);
});
it("keeps plugin-owned catalog tools on the plugin construction path", () => {
expectConstructionPlan(resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["canvas"] }), {
includeCoreTools: false,
coding: {
includeChannelTools: true,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expectConstructionPlan(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["browser"] }),
{
@@ -2,7 +2,11 @@
* Plans which core, bundle MCP, and bundle LSP tools an attempt should build.
*/
import { TOOL_NAME_SEPARATOR } from "../../agent-bundle-mcp-names.js";
import type { OpenClawCodingToolConstructionPlan } from "../../agent-tools.js";
import {
type CoreToolFactoryFamily,
type OpenClawCodingToolConstructionPlan,
resolveCoreToolFactoryFamily,
} from "../../core-tool-factory-descriptors.js";
import { isToolAllowedByPolicyName } from "../../tool-policy-match.js";
import {
buildPluginToolGroups,
@@ -12,48 +16,6 @@ import {
normalizeToolName,
} from "../../tool-policy.js";
const BASE_CODING_TOOL_FACTORY_NAMES = new Set(["edit", "read", "write"]);
const SHELL_CODING_TOOL_FACTORY_NAMES = new Set(["apply_patch", "exec", "process"]);
// Names here must be emitted directly by createOpenClawTools(). Catalog entries
// backed by plugin registration, such as browser/x_search/code_execution, stay
// out of this set so narrow allowlists still materialize plugin tools.
const OPENCLAW_TOOL_FACTORY_NAMES = new Set([
"agents_list",
"crestodian",
"canvas",
"computer",
"cron",
"gateway",
"get_goal",
"heartbeat_respond",
"heartbeat_response",
"image",
"image_generate",
"message",
"music_generate",
"nodes",
"pdf",
"session_status",
"sessions_history",
"sessions_list",
"sessions_send",
"sessions_spawn",
"sessions_yield",
"skill_workshop",
"spawn_task",
"create_goal",
"subagents",
"tts",
"update_goal",
"update_plan",
"dismiss_task",
"video_generate",
"web_fetch",
"web_search",
]);
const ALL_CODING_TOOL_CONSTRUCTION_PLAN: OpenClawCodingToolConstructionPlan = {
includeBaseCodingTools: true,
includeShellTools: true,
@@ -89,16 +51,6 @@ function hasWildcardToolAllowlist(toolsAllow: string[]): boolean {
return toolsAllow.some((entry) => normalizeToolName(entry) === "*");
}
function isKnownLocalCodingToolName(normalized: string): boolean {
// Unknown non-bundle names are treated as plugin tools so installed plugin
// catalog entries still materialize under narrow allowlists.
return (
BASE_CODING_TOOL_FACTORY_NAMES.has(normalized) ||
SHELL_CODING_TOOL_FACTORY_NAMES.has(normalized) ||
OPENCLAW_TOOL_FACTORY_NAMES.has(normalized)
);
}
/**
* Applies a runtime allowlist to a concrete tool list after expanding tool and
* plugin groups. Undefined allowlists keep all tools; an explicit empty list
@@ -166,17 +118,22 @@ function resolveCodingToolConstructionPlanForAllowlist(
}
const expanded = expandToolGroups(toolsAllow);
const normalized = normalizeToolList(expanded);
const includeBaseCodingTools = normalized.some((name) =>
BASE_CODING_TOOL_FACTORY_NAMES.has(name),
);
const includeShellTools = normalized.some((name) => SHELL_CODING_TOOL_FACTORY_NAMES.has(name));
const includeOpenClawTools = normalized.some((name) => OPENCLAW_TOOL_FACTORY_NAMES.has(name));
const includePluginTools = normalized.some(
(name) =>
name === "group:plugins" ||
// Plugin ids/tool names are not known to this local factory list at build time.
(!isBundleMcpAllowlistName(name) && !isKnownLocalCodingToolName(name)),
);
const coreFamilies = new Set<CoreToolFactoryFamily>();
let includePluginTools = false;
for (const name of normalized) {
const family = resolveCoreToolFactoryFamily(name);
if (family) {
coreFamilies.add(family);
continue;
}
// Plugin ids/tool names are not known to the local factory catalog.
if (!isBundleMcpAllowlistName(name)) {
includePluginTools = true;
}
}
const includeBaseCodingTools = coreFamilies.has("base-coding");
const includeShellTools = coreFamilies.has("shell");
const includeOpenClawTools = coreFamilies.has("openclaw");
// Channel delivery tools are constructed through plugin-capable runtime setup.
const includeChannelTools = includePluginTools;
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { setEmbeddedMode } from "../infra/embedded-mode.js";
import { isToolWrappedWithBeforeToolCallHook } from "./agent-tools.before-tool-call.js";
import { CORE_TOOL_FACTORY_DESCRIPTORS } from "./core-tool-factory-descriptors.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import { shouldIncludeUpdatePlanToolForOpenClawTools } from "./openclaw-tools.registration.js";
import { createUpdatePlanTool } from "./tools/update-plan-tool.js";
@@ -64,6 +65,26 @@ describe("openclaw-tools update_plan gating", () => {
setEmbeddedMode(false);
});
it("keeps concrete OpenClaw tool names in the factory descriptor catalog", () => {
const describedNames = new Set<string>(
CORE_TOOL_FACTORY_DESCRIPTORS.filter((descriptor) => descriptor.family === "openclaw").map(
(descriptor) => descriptor.name,
),
);
const emittedNames = createFastToolNames({
agentSessionKey: "agent:main:main",
config: {
tools: { allow: ["update_plan"] },
transcripts: { enabled: true },
} as OpenClawConfig,
cwd: "/repo",
enableHeartbeatTool: true,
taskSuggestionDeliveryMode: "gateway",
});
expect(emittedNames.filter((name) => !describedNames.has(name))).toEqual([]);
});
it("keeps update_plan disabled by default", () => {
expectUpdatePlanEnabled({ config: {} as OpenClawConfig }, false);
});