mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
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:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user