mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:20:44 +00:00
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:
committed by
GitHub
parent
c84b7cbffc
commit
25b30c9520
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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_");
|
||||
});
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user