fix: honor embedded runtime tool allowlists (#77609)

* fix: honor embedded runtime tool allowlists

* fix: preserve plugin allowlist filtering

* fix: gate bundled lsp allowlists
This commit is contained in:
Peter Steinberger
2026-05-05 02:29:36 +01:00
committed by GitHub
parent c84b7cbffc
commit 25b30c9520
9 changed files with 874 additions and 286 deletions

View File

@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Media/Windows: open saved attachment temp files read/write before fsync so Windows WebChat and `chat.send` media offloads no longer fail with EPERM during durability flush. (#76593) Thanks @qq230849622-a11y.
- Agents/tools: honor narrow runtime tool allowlists when constructing embedded-runner tool families and bundled MCP/LSP runtimes, so cron/subagent runs that request tools such as `update_plan`, `browser`, `x_search`, channel login tools, or `group:plugins` no longer start with missing tools or unrelated bootstrap work. (#77519, #77532)
- Codex plugin: mirror the experimental upstream app-server protocol and format generated TypeScript before drift checks, keeping OpenClaw's `experimentalApi` bridge compatible with latest Codex while preserving formatter gates.
- Telegram/media: derive no-caption inbound media placeholders from saved MIME metadata instead of the Telegram `photo` shape, so non-image and mixed attachments no longer reach the model as `<media:image>`. Fixes #69793. Thanks @aspalagin.
- Agents/cache: keep per-turn runtime context out of ordinary chat system prompts while still delivering hidden current-turn context, restoring prompt-cache reuse on chat continuations. Fixes #77431. Thanks @Udjin79.

View File

@@ -94,6 +94,17 @@ function isToolAllowedByFactoryPolicy(params: {
});
}
function isToolExplicitlyAllowedByFactoryPolicy(params: {
toolName: string;
allowlist?: string[];
denylist?: string[];
}): boolean {
if (!params.allowlist?.some((entry) => typeof entry === "string" && entry.trim().length > 0)) {
return false;
}
return isToolAllowedByFactoryPolicy(params);
}
function mergeFactoryPolicyList(...lists: Array<string[] | undefined>): string[] | undefined {
const merged = lists.flatMap((list) => (Array.isArray(list) ? list : []));
return merged.length > 0 ? Array.from(new Set(merged)) : undefined;
@@ -499,6 +510,19 @@ export function createOpenClawTools(
const effectiveCallGateway = embedded
? createEmbeddedCallGateway()
: openClawToolsDeps.callGateway;
const includeUpdatePlanTool =
isToolExplicitlyAllowedByFactoryPolicy({
toolName: "update_plan",
allowlist: mergeFactoryPolicyList(resolvedConfig?.tools?.allow, options?.pluginToolAllowlist),
denylist: mergeFactoryPolicyList(resolvedConfig?.tools?.deny, options?.pluginToolDenylist),
}) ||
isUpdatePlanToolEnabledForOpenClawTools({
config: resolvedConfig,
agentSessionKey: options?.agentSessionKey,
agentId: options?.requesterAgentIdOverride,
modelProvider: options?.modelProvider,
modelId: options?.modelId,
});
const tools: AnyAgentTool[] = [
...(embedded
? []
@@ -539,15 +563,7 @@ export function createOpenClawTools(
agentSessionKey: options?.agentSessionKey,
requesterAgentIdOverride: options?.requesterAgentIdOverride,
}),
...(isUpdatePlanToolEnabledForOpenClawTools({
config: resolvedConfig,
agentSessionKey: options?.agentSessionKey,
agentId: options?.requesterAgentIdOverride,
modelProvider: options?.modelProvider,
modelId: options?.modelId,
})
? [createUpdatePlanTool()]
: []),
...(includeUpdatePlanTool ? [createUpdatePlanTool()] : []),
createSessionsListTool({
agentSessionKey: options?.agentSessionKey,
sandboxed: options?.sandboxed,

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import { isUpdatePlanToolEnabledForOpenClawTools } from "./openclaw-tools.registration.js";
import { createUpdatePlanTool } from "./tools/update-plan-tool.js";
@@ -31,6 +32,25 @@ describe("openclaw-tools update_plan gating", () => {
expectUpdatePlanEnabled({ config: {} as OpenClawConfig }, false);
});
it("does not expose update_plan from default tool construction", () => {
const defaultTools = createOpenClawTools({
config: {} as OpenClawConfig,
disablePluginTools: true,
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
const emptyAllowlistTools = createOpenClawTools({
config: {} as OpenClawConfig,
disablePluginTools: true,
pluginToolAllowlist: [],
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
expect(defaultTools.some((tool) => tool.name === "update_plan")).toBe(false);
expect(emptyAllowlistTools.some((tool) => tool.name === "update_plan")).toBe(false);
});
it("registers update_plan when explicitly enabled", () => {
const config = {
tools: {
@@ -44,6 +64,54 @@ describe("openclaw-tools update_plan gating", () => {
expect(createUpdatePlanTool().displaySummary).toBe("Track a short structured work plan.");
});
it("registers update_plan when the runtime allowlist explicitly requests it", () => {
const tools = createOpenClawTools({
config: {} as OpenClawConfig,
disablePluginTools: true,
pluginToolAllowlist: ["update_plan"],
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
expect(tools.some((tool) => tool.name === "update_plan")).toBe(true);
});
it("registers update_plan when a config allowlist group includes it", () => {
const tools = createOpenClawTools({
config: { tools: { allow: ["group:agents"] } } as OpenClawConfig,
disablePluginTools: true,
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
expect(tools.some((tool) => tool.name === "update_plan")).toBe(true);
});
it("registers update_plan when a runtime allowlist group includes it", () => {
const tools = createOpenClawTools({
config: {} as OpenClawConfig,
disablePluginTools: true,
pluginToolAllowlist: ["group:agents"],
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
expect(tools.some((tool) => tool.name === "update_plan")).toBe(true);
});
it("respects deny policy while constructing update_plan for grouped allowlists", () => {
const tools = createOpenClawTools({
config: {} as OpenClawConfig,
disablePluginTools: true,
pluginToolAllowlist: ["group:agents"],
pluginToolDenylist: ["update_plan"],
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
});
expect(tools.some((tool) => tool.name === "update_plan")).toBe(false);
});
it("auto-enables update_plan for unconfigured GPT-5 openai runs", () => {
// Criterion 1 of the GPT-5.4 parity gate ("no stalls after planning") is
// universal, not opt-in. Unspecified executionContract on a supported

View File

@@ -0,0 +1,303 @@
import { describe, expect, it } from "vitest";
import {
applyEmbeddedAttemptToolsAllow,
resolveEmbeddedAttemptToolConstructionPlan,
shouldBuildCoreCodingToolsForAllowlist,
shouldCreateBundleLspRuntimeForAttempt,
shouldCreateBundleMcpRuntimeForAttempt,
} from "./attempt-tool-construction-plan.js";
describe("applyEmbeddedAttemptToolsAllow", () => {
it("keeps explicit toolsAllow authoritative after force-added tools are built", () => {
const tools = [{ name: "exec" }, { name: "read" }, { name: "message" }];
expect(
applyEmbeddedAttemptToolsAllow(tools, ["exec", "read"]).map((tool) => tool.name),
).toEqual(["exec", "read"]);
});
it("normalizes explicit toolsAllow entries before filtering", () => {
const tools = [{ name: "cron" }, { name: "read" }, { name: "message" }];
expect(
applyEmbeddedAttemptToolsAllow(tools, [" cron ", "READ"]).map((tool) => tool.name),
).toEqual(["cron", "read"]);
});
it("honors wildcard and group allowlists in the final filter", () => {
const tools = [{ name: "exec" }, { name: "read" }, { name: "message" }];
expect(applyEmbeddedAttemptToolsAllow(tools, ["*"]).map((tool) => tool.name)).toEqual([
"exec",
"read",
"message",
]);
expect(applyEmbeddedAttemptToolsAllow(tools, ["group:fs"]).map((tool) => tool.name)).toEqual([
"read",
]);
});
it("keeps plugin-only allowlists on the shared tool policy path", () => {
const tools = [{ name: "memory_search" }, { name: "plugin_extra" }];
expect(shouldBuildCoreCodingToolsForAllowlist(["memory_search"])).toBe(false);
expect(
applyEmbeddedAttemptToolsAllow(tools, ["memory_search"]).map((tool) => tool.name),
).toEqual(["memory_search"]);
});
it("expands plugin group and plugin-id allowlists before the final filter", () => {
const tools = [
{ name: "exec" },
{ name: "memory_search" },
{ name: "memory_get" },
{ name: "browser" },
];
const toolMeta = (tool: { name: string }) => {
if (tool.name.startsWith("memory_")) {
return { pluginId: "active-memory" };
}
if (tool.name === "browser") {
return { pluginId: "browser" };
}
return undefined;
};
expect(
applyEmbeddedAttemptToolsAllow(tools, ["group:plugins"], { toolMeta }).map(
(tool) => tool.name,
),
).toEqual(["memory_search", "memory_get", "browser"]);
expect(
applyEmbeddedAttemptToolsAllow(tools, ["active-memory"], { toolMeta }).map(
(tool) => tool.name,
),
).toEqual(["memory_search", "memory_get"]);
});
it("treats an explicit empty toolsAllow as no tools", () => {
const tools = [{ name: "exec" }, { name: "read" }, { name: "message" }];
expect(applyEmbeddedAttemptToolsAllow(tools, []).map((tool) => tool.name)).toEqual([]);
expect(shouldBuildCoreCodingToolsForAllowlist([])).toBe(false);
});
});
describe("resolveEmbeddedAttemptToolConstructionPlan", () => {
it("builds all tool families when no runtime allowlist is present", () => {
expect(resolveEmbeddedAttemptToolConstructionPlan({})).toMatchObject({
constructTools: true,
includeCoreTools: true,
codingToolConstructionPlan: {
includeBaseCodingTools: true,
includeShellTools: true,
includeChannelTools: true,
includeOpenClawTools: true,
includePluginTools: true,
},
});
});
it("short-circuits all local tool construction for explicit no-tools runs", () => {
expect(resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: [] })).toMatchObject({
constructTools: false,
includeCoreTools: false,
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: false,
},
});
});
it("materializes only plugin candidates for plugin-only allowlists", () => {
expect(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["memory_search"] }),
).toMatchObject({
constructTools: true,
includeCoreTools: false,
runtimeToolAllowlist: ["memory_search"],
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: true,
includeOpenClawTools: false,
includePluginTools: true,
},
});
});
it("limits known core allowlists to the matching local families", () => {
expect(resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["read"] })).toMatchObject({
constructTools: true,
includeCoreTools: true,
codingToolConstructionPlan: {
includeBaseCodingTools: true,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: false,
},
});
expect(resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["exec"] })).toMatchObject({
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: true,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: false,
},
});
expect(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["session_status"] }),
).toMatchObject({
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["update_plan"] }),
).toMatchObject({
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
});
it("keeps plugin-owned catalog tools on the plugin construction path", () => {
expect(resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["browser"] })).toMatchObject({
constructTools: true,
includeCoreTools: false,
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: true,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expect(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["code_execution"] }),
).toMatchObject({
constructTools: true,
includeCoreTools: false,
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: true,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expect(resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["x_search"] })).toMatchObject({
includeCoreTools: false,
codingToolConstructionPlan: {
includeChannelTools: true,
includeOpenClawTools: false,
includePluginTools: true,
},
});
});
it("keeps channel tools available for narrow channel-owned allowlists", () => {
expect(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["whatsapp_login"] }),
).toMatchObject({
constructTools: true,
includeCoreTools: false,
codingToolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: true,
includeOpenClawTools: false,
includePluginTools: true,
},
});
});
it("skips local construction when only bundled tool runtimes can match", () => {
expect(
resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow: ["strict__strict_probe"] }),
).toMatchObject({
constructTools: false,
includeCoreTools: false,
});
});
});
describe("shouldCreateBundleMcpRuntimeForAttempt", () => {
it("skips bundle MCP runtime when tools are disabled", () => {
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: false })).toBe(false);
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true, disableTools: true })).toBe(
false,
);
});
it("creates bundle MCP only when the allowlist can reach bundle MCP tool names", () => {
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true })).toBe(true);
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true, toolsAllow: ["*"] })).toBe(
true,
);
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true, toolsAllow: [] })).toBe(
false,
);
expect(
shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["memory_search", "memory_get"],
}),
).toBe(false);
expect(
shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["group:plugins"],
}),
).toBe(true);
expect(
shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["bundle-mcp"],
}),
).toBe(true);
expect(
shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["strict__strict_probe"],
}),
).toBe(true);
});
});
describe("shouldCreateBundleLspRuntimeForAttempt", () => {
it("skips bundle LSP startup when runtime allowlists cannot reach LSP tools", () => {
expect(shouldCreateBundleLspRuntimeForAttempt({ toolsEnabled: true })).toBe(true);
expect(shouldCreateBundleLspRuntimeForAttempt({ toolsEnabled: true, toolsAllow: ["*"] })).toBe(
true,
);
expect(shouldCreateBundleLspRuntimeForAttempt({ toolsEnabled: true, toolsAllow: [] })).toBe(
false,
);
expect(
shouldCreateBundleLspRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["memory_search"],
}),
).toBe(false);
expect(
shouldCreateBundleLspRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["lsp_hover_typescript"],
}),
).toBe(true);
});
});

View File

@@ -0,0 +1,232 @@
import { TOOL_NAME_SEPARATOR } from "../../pi-bundle-mcp-names.js";
import type { OpenClawCodingToolConstructionPlan } from "../../pi-tools.js";
import { isToolAllowedByPolicyName } from "../../tool-policy-match.js";
import {
buildPluginToolGroups,
expandPolicyWithPluginGroups,
expandToolGroups,
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",
"canvas",
"cron",
"gateway",
"heartbeat_respond",
"heartbeat_response",
"image",
"image_generate",
"message",
"music_generate",
"nodes",
"pdf",
"session_status",
"sessions_history",
"sessions_list",
"sessions_send",
"sessions_spawn",
"sessions_yield",
"subagents",
"tts",
"update_plan",
"video_generate",
"web_fetch",
"web_search",
]);
const ALL_CODING_TOOL_CONSTRUCTION_PLAN: OpenClawCodingToolConstructionPlan = {
includeBaseCodingTools: true,
includeShellTools: true,
includeChannelTools: true,
includeOpenClawTools: true,
includePluginTools: true,
};
const NO_CODING_TOOL_CONSTRUCTION_PLAN: OpenClawCodingToolConstructionPlan = {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: false,
};
function cloneCodingToolConstructionPlan(
plan: OpenClawCodingToolConstructionPlan,
): OpenClawCodingToolConstructionPlan {
return { ...plan };
}
function isBundleMcpAllowlistName(normalized: string): boolean {
return normalized === "bundle-mcp" || normalized.includes(TOOL_NAME_SEPARATOR);
}
function isPluginGroupAllowlistName(normalized: string): boolean {
return normalized === "group:plugins";
}
function hasWildcardToolAllowlist(toolsAllow: string[]): boolean {
return toolsAllow.some((entry) => normalizeToolName(entry) === "*");
}
function isKnownLocalCodingToolName(normalized: string): boolean {
return (
BASE_CODING_TOOL_FACTORY_NAMES.has(normalized) ||
SHELL_CODING_TOOL_FACTORY_NAMES.has(normalized) ||
OPENCLAW_TOOL_FACTORY_NAMES.has(normalized)
);
}
export function applyEmbeddedAttemptToolsAllow<T extends { name: string }>(
tools: T[],
toolsAllow?: string[],
options?: {
toolMeta?: (tool: T) => { pluginId: string } | undefined;
},
): T[] {
if (!toolsAllow) {
return tools;
}
if (toolsAllow.length === 0) {
return [];
}
if (hasWildcardToolAllowlist(toolsAllow)) {
return tools;
}
const pluginGroups = options?.toolMeta
? buildPluginToolGroups({ tools, toolMeta: options.toolMeta })
: undefined;
const policy = pluginGroups
? expandPolicyWithPluginGroups({ allow: toolsAllow }, pluginGroups)
: { allow: toolsAllow };
return tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy));
}
function resolveCodingToolConstructionPlanForAllowlist(
toolsAllow?: string[],
): OpenClawCodingToolConstructionPlan {
if (!toolsAllow) {
return cloneCodingToolConstructionPlan(ALL_CODING_TOOL_CONSTRUCTION_PLAN);
}
if (toolsAllow.length === 0) {
return cloneCodingToolConstructionPlan(NO_CODING_TOOL_CONSTRUCTION_PLAN);
}
if (hasWildcardToolAllowlist(toolsAllow)) {
return cloneCodingToolConstructionPlan(ALL_CODING_TOOL_CONSTRUCTION_PLAN);
}
const expanded = expandToolGroups(toolsAllow);
const normalized = expanded.map((entry) => normalizeToolName(entry)).filter(Boolean);
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" ||
(!isBundleMcpAllowlistName(name) && !isKnownLocalCodingToolName(name)),
);
const includeChannelTools = includePluginTools;
return {
includeBaseCodingTools,
includeShellTools,
includeChannelTools,
includeOpenClawTools,
includePluginTools,
};
}
export function resolveEmbeddedAttemptToolConstructionPlan(params: {
disableTools?: boolean;
isRawModelRun?: boolean;
toolsAllow?: string[];
}): {
constructTools: boolean;
includeCoreTools: boolean;
runtimeToolAllowlist?: string[];
codingToolConstructionPlan: OpenClawCodingToolConstructionPlan;
} {
if (params.disableTools === true || params.isRawModelRun === true) {
return {
constructTools: false,
includeCoreTools: false,
codingToolConstructionPlan: cloneCodingToolConstructionPlan(NO_CODING_TOOL_CONSTRUCTION_PLAN),
};
}
const codingToolConstructionPlan = resolveCodingToolConstructionPlanForAllowlist(
params.toolsAllow,
);
const includeCoreTools =
codingToolConstructionPlan.includeBaseCodingTools ||
codingToolConstructionPlan.includeShellTools ||
codingToolConstructionPlan.includeOpenClawTools;
const constructTools =
includeCoreTools ||
codingToolConstructionPlan.includeChannelTools ||
codingToolConstructionPlan.includePluginTools;
return {
constructTools,
includeCoreTools,
...(params.toolsAllow ? { runtimeToolAllowlist: params.toolsAllow } : {}),
codingToolConstructionPlan,
};
}
export function shouldBuildCoreCodingToolsForAllowlist(toolsAllow?: string[]): boolean {
return resolveEmbeddedAttemptToolConstructionPlan({ toolsAllow }).includeCoreTools;
}
export function shouldCreateBundleMcpRuntimeForAttempt(params: {
toolsEnabled: boolean;
disableTools?: boolean;
toolsAllow?: string[];
}): boolean {
if (!params.toolsEnabled || params.disableTools === true) {
return false;
}
if (!params.toolsAllow) {
return true;
}
if (params.toolsAllow.length === 0) {
return false;
}
if (hasWildcardToolAllowlist(params.toolsAllow)) {
return true;
}
return params.toolsAllow.some((toolName) => {
const normalized = normalizeToolName(toolName);
return isBundleMcpAllowlistName(normalized) || isPluginGroupAllowlistName(normalized);
});
}
export function shouldCreateBundleLspRuntimeForAttempt(params: {
toolsEnabled: boolean;
disableTools?: boolean;
toolsAllow?: string[];
}): boolean {
if (!params.toolsEnabled || params.disableTools === true) {
return false;
}
if (!params.toolsAllow) {
return true;
}
if (params.toolsAllow.length === 0) {
return false;
}
if (hasWildcardToolAllowlist(params.toolsAllow)) {
return true;
}
return params.toolsAllow.some((toolName) => {
const normalized = normalizeToolName(toolName);
return normalized.startsWith("lsp_");
});
}

View File

@@ -10,7 +10,6 @@ import {
buildAfterTurnRuntimeContextFromUsage,
composeSystemPromptWithHookContext,
decodeHtmlEntitiesInObject,
applyEmbeddedAttemptToolsAllow,
isPrimaryBootstrapRun,
mergeOrphanedTrailingUserPrompt,
normalizeMessagesForLlmBoundary,
@@ -21,8 +20,6 @@ import {
resolveAttemptFsWorkspaceOnly,
resolveEmbeddedAgentStreamFn,
resolveUnknownToolGuardThreshold,
shouldCreateBundleMcpRuntimeForAttempt,
shouldBuildCoreCodingToolsForAllowlist,
resolveAttemptToolPolicyMessageProvider,
resolvePromptBuildHookResult,
resolvePromptModeForSession,
@@ -66,33 +63,6 @@ async function invokeWrappedTestStream(
return await Promise.resolve(wrappedFn({} as never, {} as never, {} as never));
}
describe("applyEmbeddedAttemptToolsAllow", () => {
it("keeps explicit toolsAllow authoritative after force-added tools are built", () => {
const tools = [{ name: "exec" }, { name: "read" }, { name: "message" }];
expect(
applyEmbeddedAttemptToolsAllow(tools, ["exec", "read"]).map((tool) => tool.name),
).toEqual(["exec", "read"]);
});
it("normalizes explicit toolsAllow entries before filtering", () => {
const tools = [{ name: "cron" }, { name: "read" }, { name: "message" }];
expect(
applyEmbeddedAttemptToolsAllow(tools, [" cron ", "READ"]).map((tool) => tool.name),
).toEqual(["cron", "read"]);
});
it("keeps plugin-only allowlists on the shared tool policy path", () => {
const tools = [{ name: "memory_search" }, { name: "plugin_extra" }];
expect(shouldBuildCoreCodingToolsForAllowlist(["memory_search"])).toBe(false);
expect(
applyEmbeddedAttemptToolsAllow(tools, ["memory_search"]).map((tool) => tool.name),
).toEqual(["memory_search"]);
});
});
describe("buildEmbeddedAttemptToolRunContext", () => {
it("carries runtime toolsAllow into coding tool construction", () => {
expect(
@@ -181,40 +151,6 @@ describe("normalizeMessagesForLlmBoundary", () => {
});
});
describe("shouldCreateBundleMcpRuntimeForAttempt", () => {
it("skips bundle MCP when tools are disabled or unavailable", () => {
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: false })).toBe(false);
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true, disableTools: true })).toBe(
false,
);
});
it("creates bundle MCP only when the allowlist can reach bundle MCP tool names", () => {
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true })).toBe(true);
expect(shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true, toolsAllow: [] })).toBe(
true,
);
expect(
shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["memory_search", "memory_get"],
}),
).toBe(false);
expect(
shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["bundle-mcp"],
}),
).toBe(true);
expect(
shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled: true,
toolsAllow: ["strict__strict_probe"],
}),
).toBe(true);
});
});
describe("resolveAttemptToolPolicyMessageProvider", () => {
it("prefers explicit tool-policy provider over transport channel", () => {
expect(

View File

@@ -88,7 +88,6 @@ import { supportsModelTools } from "../../model-tool-support.js";
import { releaseWsSession } from "../../openai-ws-stream.js";
import { resolveOwnerDisplaySetting } from "../../owner-display.js";
import { createBundleLspToolRuntime } from "../../pi-bundle-lsp-runtime.js";
import { TOOL_NAME_SEPARATOR } from "../../pi-bundle-mcp-names.js";
import {
getOrCreateSessionMcpRuntime,
materializeBundleMcpToolsForRun,
@@ -162,7 +161,6 @@ import {
collectExplicitToolAllowlistSources,
} from "../../tool-allowlist-guard.js";
import { UNKNOWN_TOOL_THRESHOLD } from "../../tool-loop-detection.js";
import { normalizeToolName } from "../../tool-policy.js";
import { shouldAllowProviderOwnedThinkingReplay } from "../../transcript-policy.js";
import { normalizeUsage, type NormalizedUsage } from "../../usage.js";
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
@@ -234,6 +232,12 @@ import { mapThinkingLevel } from "../utils.js";
import { flushPendingToolResultsAfterIdle } from "../wait-for-idle-before-flush.js";
import { abortable as abortableWithSignal } from "./abortable.js";
import { createEmbeddedAgentSessionWithResourceLoader } from "./attempt-session.js";
import {
applyEmbeddedAttemptToolsAllow,
resolveEmbeddedAttemptToolConstructionPlan,
shouldCreateBundleLspRuntimeForAttempt,
shouldCreateBundleMcpRuntimeForAttempt,
} from "./attempt-tool-construction-plan.js";
export { buildContextEnginePromptCacheInfo } from "./attempt.context-engine-helpers.js";
import {
rotateTranscriptAfterCompaction,
@@ -483,65 +487,6 @@ function summarizeSessionContext(messages: AgentMessage[]): {
};
}
export function applyEmbeddedAttemptToolsAllow<T extends { name: string }>(
tools: T[],
toolsAllow?: string[],
): T[] {
if (!toolsAllow || toolsAllow.length === 0) {
return tools;
}
const allowSet = new Set(toolsAllow.map((name) => normalizeToolName(name)));
return tools.filter((tool) => allowSet.has(normalizeToolName(tool.name)));
}
const CORE_CODING_TOOL_ALLOWLIST_NAMES = new Set([
"agents_list",
"apply_patch",
"bash",
"canvas",
"cron",
"edit",
"exec",
"gateway",
"heartbeat_response",
"image",
"image_generate",
"message",
"music_generate",
"nodes",
"pdf",
"read",
"session_status",
"sessions_history",
"sessions_list",
"sessions_send",
"sessions_spawn",
"sessions_yield",
"subagents",
"tts",
"update_plan",
"video_generate",
"web_fetch",
"web_search",
"write",
]);
export function shouldBuildCoreCodingToolsForAllowlist(toolsAllow?: string[]): boolean {
if (!toolsAllow || toolsAllow.length === 0) {
return true;
}
return toolsAllow.some((toolName) => {
const normalized = normalizeToolName(toolName);
return (
normalized === "*" ||
normalized.startsWith("group:") ||
normalized === "bundle-mcp" ||
normalized.includes(TOOL_NAME_SEPARATOR) ||
CORE_CODING_TOOL_ALLOWLIST_NAMES.has(normalized)
);
});
}
export function normalizeMessagesForLlmBoundary(messages: AgentMessage[]): AgentMessage[] {
const normalized = stripToolResultDetails(normalizeAssistantReplayContent(messages));
return stripHistoricalRuntimeContextCustomMessages(normalized);
@@ -598,22 +543,6 @@ function removeTrailingMidTurnPrecheckAssistantError(params: {
mutableSessionManager._rewriteFile();
}
export function shouldCreateBundleMcpRuntimeForAttempt(params: {
toolsEnabled: boolean;
disableTools?: boolean;
toolsAllow?: string[];
}): boolean {
if (!params.toolsEnabled || params.disableTools === true) {
return false;
}
if (!params.toolsAllow || params.toolsAllow.length === 0) {
return true;
}
return params.toolsAllow.some(
(toolName) => toolName === "bundle-mcp" || toolName.includes(TOOL_NAME_SEPARATOR),
);
}
export function resolveAttemptToolPolicyMessageProvider(params: {
messageProvider?: string;
messageChannel?: string;
@@ -857,91 +786,98 @@ export async function runEmbeddedAttempt(
});
};
const corePluginToolStages = createEmbeddedRunStageTracker();
const toolsRaw =
params.disableTools || isRawModelRun
? []
: (() => {
const allTools = createOpenClawCodingTools({
agentId: sessionAgentId,
...buildEmbeddedAttemptToolRunContext({ ...params, trace: runTrace }),
exec: {
...params.execOverrides,
elevated: params.bashElevated,
},
const toolConstructionPlan = resolveEmbeddedAttemptToolConstructionPlan({
disableTools: params.disableTools,
isRawModelRun,
toolsAllow: params.toolsAllow,
});
const toolsRaw = !toolConstructionPlan.constructTools
? []
: (() => {
const allTools = createOpenClawCodingTools({
agentId: sessionAgentId,
...buildEmbeddedAttemptToolRunContext({ ...params, trace: runTrace }),
exec: {
...params.execOverrides,
elevated: params.bashElevated,
},
sandbox,
messageProvider: resolveAttemptToolPolicyMessageProvider(params),
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
memberRoleIds: params.memberRoleIds,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
senderIsOwner: params.senderIsOwner,
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
sessionKey: sandboxSessionKey,
// When sandboxSessionKey differs from the real run session key (e.g. Telegram
// direct peer key vs agent:main:main), pass the live key so session_status
// "current" resolves to the active run session, not the stale sandbox key.
runSessionKey:
params.sessionKey && params.sessionKey !== sandboxSessionKey
? params.sessionKey
: undefined,
sessionId: params.sessionId,
runId: params.runId,
agentDir,
workspaceDir: effectiveWorkspace,
// When sandboxing uses a copied workspace (`ro` or `none`), effectiveWorkspace points
// at the sandbox copy. Spawned subagents should inherit the real workspace instead.
spawnWorkspaceDir: resolveAttemptSpawnWorkspaceDir({
sandbox,
messageProvider: resolveAttemptToolPolicyMessageProvider(params),
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
memberRoleIds: params.memberRoleIds,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
senderIsOwner: params.senderIsOwner,
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
sessionKey: sandboxSessionKey,
// When sandboxSessionKey differs from the real run session key (e.g. Telegram
// direct peer key vs agent:main:main), pass the live key so session_status
// "current" resolves to the active run session, not the stale sandbox key.
runSessionKey:
params.sessionKey && params.sessionKey !== sandboxSessionKey
? params.sessionKey
: undefined,
sessionId: params.sessionId,
runId: params.runId,
agentDir,
resolvedWorkspace,
}),
config: params.config,
abortSignal: runAbortController.signal,
modelProvider: params.provider,
modelId: params.modelId,
modelCompat: extractModelCompat(params.model),
modelApi: params.model.api,
modelContextWindowTokens: params.model.contextWindow,
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
workspaceDir: effectiveWorkspace,
// When sandboxing uses a copied workspace (`ro` or `none`), effectiveWorkspace points
// at the sandbox copy. Spawned subagents should inherit the real workspace instead.
spawnWorkspaceDir: resolveAttemptSpawnWorkspaceDir({
sandbox,
resolvedWorkspace,
}),
config: params.config,
abortSignal: runAbortController.signal,
modelProvider: params.provider,
modelId: params.modelId,
modelCompat: extractModelCompat(params.model),
modelApi: params.model.api,
modelContextWindowTokens: params.model.contextWindow,
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
workspaceDir: effectiveWorkspace,
}),
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
includeCoreTools: shouldBuildCoreCodingToolsForAllowlist(params.toolsAllow),
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
modelHasVision: params.model.input?.includes("image") ?? false,
requireExplicitMessageTarget:
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
disableMessageTool: params.disableMessageTool,
forceMessageTool: params.forceMessageTool,
enableHeartbeatTool: params.enableHeartbeatTool,
forceHeartbeatTool: params.forceHeartbeatTool,
authProfileStore: params.authProfileStore,
recordToolPrepStage: (name) => corePluginToolStages.mark(name),
onToolOutcome: params.onToolOutcome,
onYield: (message) => {
yieldDetected = true;
yieldMessage = message;
queueYieldInterruptForSession?.();
runAbortController.abort("sessions_yield");
abortSessionForYield?.();
},
});
corePluginToolStages.mark("attempt:create-openclaw-coding-tools");
const filteredTools = applyEmbeddedAttemptToolsAllow(allTools, params.toolsAllow);
corePluginToolStages.mark("attempt:tools-allow");
return filteredTools;
})();
}),
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
includeCoreTools: toolConstructionPlan.includeCoreTools,
toolConstructionPlan: toolConstructionPlan.codingToolConstructionPlan,
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
modelHasVision: params.model.input?.includes("image") ?? false,
requireExplicitMessageTarget:
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
disableMessageTool: params.disableMessageTool,
forceMessageTool: params.forceMessageTool,
enableHeartbeatTool: params.enableHeartbeatTool,
forceHeartbeatTool: params.forceHeartbeatTool,
authProfileStore: params.authProfileStore,
recordToolPrepStage: (name) => corePluginToolStages.mark(name),
onToolOutcome: params.onToolOutcome,
onYield: (message) => {
yieldDetected = true;
yieldMessage = message;
queueYieldInterruptForSession?.();
runAbortController.abort("sessions_yield");
abortSessionForYield?.();
},
});
corePluginToolStages.mark("attempt:create-openclaw-coding-tools");
const filteredTools = applyEmbeddedAttemptToolsAllow(allTools, params.toolsAllow, {
toolMeta: (tool) => getPluginToolMeta(tool),
});
corePluginToolStages.mark("attempt:tools-allow");
return filteredTools;
})();
prepStages.mark("core-plugin-tools");
emitCorePluginToolStageSummary("core-plugin-tools", corePluginToolStages.snapshot());
const toolsEnabled = supportsModelTools(params.model);
@@ -1104,18 +1040,22 @@ export async function runEmbeddedAttempt(
],
})
: undefined;
const bundleLspRuntime =
toolsEnabled && !isRawModelRun
? await createBundleLspToolRuntime({
workspaceDir: effectiveWorkspace,
cfg: params.config,
reservedToolNames: [
...tools.map((tool) => tool.name),
...(clientTools?.map((tool) => tool.function.name) ?? []),
...(bundleMcpRuntime?.tools.map((tool) => tool.name) ?? []),
],
})
: undefined;
const bundleLspEnabled = shouldCreateBundleLspRuntimeForAttempt({
toolsEnabled,
disableTools: params.disableTools || isRawModelRun,
toolsAllow: params.toolsAllow,
});
const bundleLspRuntime = bundleLspEnabled
? await createBundleLspToolRuntime({
workspaceDir: effectiveWorkspace,
cfg: params.config,
reservedToolNames: [
...tools.map((tool) => tool.name),
...(clientTools?.map((tool) => tool.function.name) ?? []),
...(bundleMcpRuntime?.tools.map((tool) => tool.name) ?? []),
],
})
: undefined;
const filteredBundledTools = applyFinalEffectiveToolPolicy({
bundledTools: [...(bundleMcpRuntime?.tools ?? []), ...(bundleLspRuntime?.tools ?? [])],
config: params.config,

View File

@@ -180,6 +180,74 @@ describe("createOpenClawCodingTools", () => {
);
});
it("skips unrelated tool families when construction is planned from a narrow allowlist", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const tools = createOpenClawCodingTools({
config: testConfig,
toolConstructionPlan: {
includeBaseCodingTools: true,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: false,
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
expect(names.has("read")).toBe(true);
expect(names.has("write")).toBe(true);
expect(names.has("edit")).toBe(true);
expect(names.has("exec")).toBe(false);
expect(names.has("process")).toBe(false);
expect(names.has("apply_patch")).toBe(false);
expect(names.has("message")).toBe(false);
});
it("passes plugin suppression into OpenClaw tool construction plans", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(createOpenClawToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
disablePluginTools: true,
}),
);
});
it("keeps plugin-only construction off the OpenClaw core factory", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
includeCoreTools: false,
runtimeToolAllowlist: ["memory_search"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
});
it("uses tools.alsoAllow for optional plugin discovery without widening to all plugins", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();

View File

@@ -243,6 +243,14 @@ export const __testing = {
applyModelProviderToolPolicy,
} as const;
export type OpenClawCodingToolConstructionPlan = {
includeBaseCodingTools: boolean;
includeShellTools: boolean;
includeChannelTools: boolean;
includeOpenClawTools: boolean;
includePluginTools: boolean;
};
export function createOpenClawCodingTools(options?: {
agentId?: string;
exec?: ExecToolDefaults & ProcessToolDefaults;
@@ -343,6 +351,8 @@ export function createOpenClawCodingTools(options?: {
forceHeartbeatTool?: boolean;
/** If false, build plugin tools only while preserving the shared policy pipeline. */
includeCoreTools?: boolean;
/** Limits which tool families are materialized before the shared policy pipeline runs. */
toolConstructionPlan?: OpenClawCodingToolConstructionPlan;
/** Whether the sender is an owner (required for owner-only tools). */
senderIsOwner?: boolean;
/**
@@ -466,6 +476,18 @@ export function createOpenClawCodingTools(options?: {
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
const workspaceRoot = resolveWorkspaceRoot(options?.workspaceDir);
const includeCoreTools = options?.includeCoreTools !== false;
const toolConstructionPlan = options?.toolConstructionPlan ?? {
includeBaseCodingTools: includeCoreTools,
includeShellTools: includeCoreTools,
includeChannelTools: includeCoreTools,
includeOpenClawTools: includeCoreTools,
includePluginTools: true,
};
const includeBaseCodingTools = includeCoreTools && toolConstructionPlan.includeBaseCodingTools;
const includeShellTools = includeCoreTools && toolConstructionPlan.includeShellTools;
const includeOpenClawTools = includeCoreTools && toolConstructionPlan.includeOpenClawTools;
const includeChannelTools = toolConstructionPlan.includeChannelTools;
const includePluginTools = toolConstructionPlan.includePluginTools;
const workspaceOnly = fsPolicy.workspaceOnly;
const applyPatchConfig = execConfig.applyPatch;
// Secure by default: apply_patch is workspace-contained unless explicitly disabled.
@@ -486,7 +508,7 @@ export function createOpenClawCodingTools(options?: {
const imageSanitization = resolveImageSanitizationLimits(options?.config);
options?.recordToolPrepStage?.("workspace-policy");
const base = includeCoreTools
const base = includeBaseCodingTools
? (createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]).flatMap((tool) => {
if (tool.name === "read") {
if (sandboxRoot) {
@@ -533,7 +555,7 @@ export function createOpenClawCodingTools(options?: {
: [];
options?.recordToolPrepStage?.("base-coding-tools");
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
const execTool = includeCoreTools
const execTool = includeShellTools
? createLazyExecTool({
...execDefaults,
host: options?.exec?.host ?? execConfig.host,
@@ -574,14 +596,14 @@ export function createOpenClawCodingTools(options?: {
: undefined,
})
: null;
const processTool = includeCoreTools
const processTool = includeShellTools
? createLazyProcessTool({
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
scopeKey,
})
: null;
const applyPatchTool =
!applyPatchEnabled || (sandboxRoot && !allowWorkspaceWrites)
!includeShellTools || !applyPatchEnabled || (sandboxRoot && !allowWorkspaceWrites)
? null
: createApplyPatchTool({
cwd: sandboxRoot ?? workspaceRoot,
@@ -615,42 +637,43 @@ export function createOpenClawCodingTools(options?: {
sandboxToolPolicy,
subagentPolicy,
]);
const pluginToolsOnly = includeCoreTools
? []
: resolveOpenClawPluginToolsForOptions({
options: {
agentSessionKey: options?.sessionKey,
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
agentAccountId: options?.agentAccountId,
agentTo: options?.messageTo,
agentThreadId: options?.messageThreadId,
agentDir: options?.agentDir,
workspaceDir: workspaceRoot,
config: options?.config,
fsPolicy,
requesterSenderId: options?.senderId,
senderIsOwner: options?.senderIsOwner,
sessionId: options?.sessionId,
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
sandboxed: !!sandbox,
pluginToolAllowlist,
pluginToolDenylist,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
currentMessageId: options?.currentMessageId,
modelProvider: options?.modelProvider,
modelHasVision: options?.modelHasVision,
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
disableMessageTool: options?.disableMessageTool,
requesterAgentIdOverride: agentId,
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
},
resolvedConfig: options?.config,
});
const pluginToolsOnly =
includeOpenClawTools || !includePluginTools
? []
: resolveOpenClawPluginToolsForOptions({
options: {
agentSessionKey: options?.sessionKey,
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
agentAccountId: options?.agentAccountId,
agentTo: options?.messageTo,
agentThreadId: options?.messageThreadId,
agentDir: options?.agentDir,
workspaceDir: workspaceRoot,
config: options?.config,
fsPolicy,
requesterSenderId: options?.senderId,
senderIsOwner: options?.senderIsOwner,
sessionId: options?.sessionId,
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
sandboxed: !!sandbox,
pluginToolAllowlist,
pluginToolDenylist,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
currentMessageId: options?.currentMessageId,
modelProvider: options?.modelProvider,
modelHasVision: options?.modelHasVision,
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
disableMessageTool: options?.disableMessageTool,
requesterAgentIdOverride: agentId,
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
},
resolvedConfig: options?.config,
});
const tools: AnyAgentTool[] = [
...base,
...(includeCoreTools && sandboxRoot
...(includeBaseCodingTools && sandboxRoot
? allowWorkspaceWrites
? [
workspaceOnly
@@ -674,12 +697,12 @@ export function createOpenClawCodingTools(options?: {
]
: []
: []),
...(includeCoreTools && applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
...(includeShellTools && applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
...(execTool ? [execTool as unknown as AnyAgentTool] : []),
...(processTool ? [processTool as unknown as AnyAgentTool] : []),
// Channel docking: include channel-defined agent tools (login, etc.).
...(includeCoreTools ? listChannelAgentTools({ cfg: options?.config }) : []),
...(includeCoreTools
...(includeChannelTools ? listChannelAgentTools({ cfg: options?.config }) : []),
...(includeOpenClawTools
? createOpenClawTools({
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
@@ -717,6 +740,7 @@ export function createOpenClawCodingTools(options?: {
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
disableMessageTool: options?.disableMessageTool,
enableHeartbeatTool,
disablePluginTools: !includePluginTools,
...(cronSelfRemoveOnlyJobId ? { cronSelfRemoveOnlyJobId } : {}),
requesterAgentIdOverride: agentId,
requesterSenderId: options?.senderId,