fix(agents): skip core tools for plugin-only allowlists

This commit is contained in:
Ayaan Zaidi
2026-05-02 08:48:05 +05:30
parent 9714eb3e65
commit 02d7ad4820
4 changed files with 338 additions and 169 deletions

View File

@@ -62,6 +62,7 @@ type AttemptSpawnWorkspaceHoisted = {
ensureGlobalUndiciEnvProxyDispatcherMock: UnknownMock;
ensureGlobalUndiciStreamTimeoutsMock: UnknownMock;
buildEmbeddedMessageActionDiscoveryInputMock: UnknownMock;
createOpenClawCodingToolsMock: UnknownMock;
subscribeEmbeddedPiSessionMock: Mock<SubscribeEmbeddedPiSessionFn>;
acquireSessionWriteLockMock: Mock<AcquireSessionWriteLockFn>;
installToolResultContextGuardMock: UnknownMock;
@@ -121,6 +122,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
const ensureGlobalUndiciEnvProxyDispatcherMock = vi.fn();
const ensureGlobalUndiciStreamTimeoutsMock = vi.fn();
const buildEmbeddedMessageActionDiscoveryInputMock = vi.fn((params: unknown) => params);
const createOpenClawCodingToolsMock = vi.fn(() => []);
const installToolResultContextGuardMock = vi.fn(() => () => {});
const installContextEngineLoopHookMock = vi.fn(() => () => {});
const flushPendingToolResultsAfterIdleMock = vi.fn(async () => {});
@@ -170,6 +172,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
ensureGlobalUndiciEnvProxyDispatcherMock,
ensureGlobalUndiciStreamTimeoutsMock,
buildEmbeddedMessageActionDiscoveryInputMock,
createOpenClawCodingToolsMock,
subscribeEmbeddedPiSessionMock,
acquireSessionWriteLockMock,
installToolResultContextGuardMock,
@@ -427,26 +430,8 @@ vi.mock("../../cache-trace.js", () => ({
}));
vi.mock("../../pi-tools.js", () => ({
createOpenClawCodingTools: (options?: { workspaceDir?: string; spawnWorkspaceDir?: string }) => [
{
name: "sessions_spawn",
execute: async (
_callId: string,
input: { task?: string },
_session?: unknown,
_abortSignal?: unknown,
_ctx?: unknown,
) =>
await hoisted.spawnSubagentDirectMock(
{
task: input.task ?? "",
},
{
workspaceDir: options?.spawnWorkspaceDir ?? options?.workspaceDir,
},
),
},
],
createOpenClawCodingTools: (options?: { workspaceDir?: string; spawnWorkspaceDir?: string }) =>
hoisted.createOpenClawCodingToolsMock(options),
resolveToolLoopDetectionConfig: () => undefined,
}));
@@ -526,6 +511,9 @@ vi.mock("../../tool-call-id.js", async (importOriginal) => {
});
vi.mock("../../tool-fs-policy.js", () => ({
createToolFsPolicy: (params: { workspaceOnly?: boolean }) => ({
workspaceOnly: params.workspaceOnly === true,
}),
resolveEffectiveToolFsWorkspaceOnly: () => false,
}));
@@ -767,6 +755,34 @@ export function resetEmbeddedAttemptHarness(
hoisted.buildEmbeddedMessageActionDiscoveryInputMock
.mockReset()
.mockImplementation((params) => params);
hoisted.createOpenClawCodingToolsMock.mockReset().mockImplementation((...args: unknown[]) => {
const options = args[0] as
| {
workspaceDir?: string;
spawnWorkspaceDir?: string;
}
| undefined;
return [
{
name: "sessions_spawn",
execute: async (
_callId: string,
input: { task?: string },
_session?: unknown,
_abortSignal?: unknown,
_ctx?: unknown,
) =>
await hoisted.spawnSubagentDirectMock(
{
task: input.task ?? "",
},
{
workspaceDir: options?.spawnWorkspaceDir ?? options?.workspaceDir,
},
),
},
];
});
hoisted.subscribeEmbeddedPiSessionMock
.mockReset()
.mockImplementation(() => createSubscriptionMock());

View File

@@ -0,0 +1,59 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
cleanupTempPaths,
createContextEngineAttemptRunner,
getHoisted,
resetEmbeddedAttemptHarness,
} from "./attempt.spawn-workspace.test-support.js";
describe("runEmbeddedAttempt toolsAllow startup cost", () => {
const tempPaths: string[] = [];
beforeEach(() => {
resetEmbeddedAttemptHarness();
});
afterEach(async () => {
await cleanupTempPaths(tempPaths);
});
it("keeps plugin-only allowlists on the shared tool policy path", async () => {
const hoisted = getHoisted();
hoisted.createOpenClawCodingToolsMock.mockReturnValue([
{
name: "memory_search",
description: "search memory",
parameters: { type: "object", properties: {} },
execute: async () => "ok",
},
{
name: "plugin_extra",
description: "extra plugin tool",
parameters: { type: "object", properties: {} },
execute: async () => "ok",
},
]);
await createContextEngineAttemptRunner({
contextEngine: {
assemble: async ({ messages }) => ({ messages, estimatedTokens: 1 }),
},
attemptOverrides: {
toolsAllow: ["memory_search"],
},
sessionKey: "agent:main:main",
tempPaths,
});
expect(hoisted.createOpenClawCodingToolsMock).toHaveBeenCalledWith(
expect.objectContaining({
includeCoreTools: false,
runtimeToolAllowlist: ["memory_search"],
}),
);
const createSessionOptions = hoisted.createAgentSessionMock.mock.calls[0]?.[0] as
| { customTools?: { name: string }[] }
| undefined;
expect(createSessionOptions?.customTools?.map((tool) => tool.name)).toEqual(["memory_search"]);
});
});

View File

@@ -496,6 +496,54 @@ export function applyEmbeddedAttemptToolsAllow<T extends { name: string }>(
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",
]);
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 stripRuntimeContextCustomMessages(normalized);
@@ -696,6 +744,10 @@ export async function runEmbeddedAttempt(
config: params.config,
agentId: params.agentId,
});
const effectiveFsWorkspaceOnly = resolveAttemptFsWorkspaceOnly({
config: params.config,
sessionAgentId,
});
prepStages.mark("workspace-sandbox");
let restoreSkillEnv: (() => void) | undefined;
@@ -833,6 +885,7 @@ export async function runEmbeddedAttempt(
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,
@@ -942,10 +995,6 @@ export async function runEmbeddedAttempt(
config: params.config,
agentId: params.agentId,
});
const effectiveFsWorkspaceOnly = resolveAttemptFsWorkspaceOnly({
config: params.config,
sessionAgentId,
});
// Track sessions_yield tool invocation (callback pattern, like clientToolCallDetected)
let yieldDetected = false;
let yieldMessage: string | null = null;

View File

@@ -22,6 +22,7 @@ import { listChannelAgentTools } from "./channel-tools.js";
import { shouldSuppressManagedWebSearchTool } from "./codex-native-web-search.js";
import { resolveImageSanitizationLimits } from "./image-sanitization.js";
import type { ModelAuthMode } from "./model-auth.js";
import { resolveOpenClawPluginToolsForOptions } from "./openclaw-plugin-tools.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import { wrapToolWithAbortSignal } from "./pi-tools.abort.js";
import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js";
@@ -352,6 +353,8 @@ export function createOpenClawCodingTools(options?: {
enableHeartbeatTool?: boolean;
/** Keep the heartbeat response tool available even when the selected profile omits it. */
forceHeartbeatTool?: boolean;
/** If false, build plugin tools only while preserving the shared policy pipeline. */
includeCoreTools?: boolean;
/** Whether the sender is an owner (required for owner-only tools). */
senderIsOwner?: boolean;
/**
@@ -467,6 +470,7 @@ export function createOpenClawCodingTools(options?: {
const sandboxFsBridge = sandbox?.fsBridge;
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
const workspaceRoot = resolveWorkspaceRoot(options?.workspaceDir);
const includeCoreTools = options?.includeCoreTools !== false;
const workspaceOnly = fsPolicy.workspaceOnly;
const applyPatchConfig = execConfig.applyPatch;
// Secure by default: apply_patch is workspace-contained unless explicitly disabled.
@@ -486,93 +490,99 @@ export function createOpenClawCodingTools(options?: {
}
const imageSanitization = resolveImageSanitizationLimits(options?.config);
const base = (createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]).flatMap((tool) => {
if (tool.name === "read") {
if (sandboxRoot) {
const sandboxed = createSandboxedReadTool({
root: sandboxRoot,
bridge: sandboxFsBridge!,
modelContextWindowTokens: options?.modelContextWindowTokens,
imageSanitization,
});
return [
workspaceOnly
? wrapToolWorkspaceRootGuardWithOptions(sandboxed, sandboxRoot, {
containerWorkdir: sandbox.containerWorkdir,
})
: sandboxed,
];
}
const freshReadTool = createReadTool(workspaceRoot);
const wrapped = createOpenClawReadTool(freshReadTool, {
modelContextWindowTokens: options?.modelContextWindowTokens,
imageSanitization,
});
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
}
if (tool.name === "bash" || tool.name === execToolName) {
return [];
}
if (tool.name === "write") {
if (sandboxRoot) {
return [];
}
const wrapped = createHostWorkspaceWriteTool(workspaceRoot, { workspaceOnly });
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
}
if (tool.name === "edit") {
if (sandboxRoot) {
return [];
}
const wrapped = createHostWorkspaceEditTool(workspaceRoot, { workspaceOnly });
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
}
return [tool];
});
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
const execTool = createLazyExecTool({
...execDefaults,
host: options?.exec?.host ?? execConfig.host,
security: options?.exec?.security ?? execConfig.security,
ask: options?.exec?.ask ?? execConfig.ask,
trigger: options?.trigger,
node: options?.exec?.node ?? execConfig.node,
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
strictInlineEval: options?.exec?.strictInlineEval ?? execConfig.strictInlineEval,
safeBinTrustedDirs: options?.exec?.safeBinTrustedDirs ?? execConfig.safeBinTrustedDirs,
safeBinProfiles: options?.exec?.safeBinProfiles ?? execConfig.safeBinProfiles,
agentId,
cwd: workspaceRoot,
allowBackground,
scopeKey,
sessionKey: options?.sessionKey,
messageProvider: options?.messageProvider,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
accountId: options?.agentAccountId,
backgroundMs: options?.exec?.backgroundMs ?? execConfig.backgroundMs,
timeoutSec: options?.exec?.timeoutSec ?? execConfig.timeoutSec,
approvalRunningNoticeMs:
options?.exec?.approvalRunningNoticeMs ?? execConfig.approvalRunningNoticeMs,
notifyOnExit: options?.exec?.notifyOnExit ?? execConfig.notifyOnExit,
notifyOnExitEmptySuccess:
options?.exec?.notifyOnExitEmptySuccess ?? execConfig.notifyOnExitEmptySuccess,
sandbox: sandbox
? {
containerName: sandbox.containerName,
workspaceDir: sandbox.workspaceDir,
containerWorkdir: sandbox.containerWorkdir,
env: sandbox.backend?.env ?? sandbox.docker.env,
buildExecSpec: sandbox.backend?.buildExecSpec.bind(sandbox.backend),
finalizeExec: sandbox.backend?.finalizeExec?.bind(sandbox.backend),
const base = includeCoreTools
? (createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]).flatMap((tool) => {
if (tool.name === "read") {
if (sandboxRoot) {
const sandboxed = createSandboxedReadTool({
root: sandboxRoot,
bridge: sandboxFsBridge!,
modelContextWindowTokens: options?.modelContextWindowTokens,
imageSanitization,
});
return [
workspaceOnly
? wrapToolWorkspaceRootGuardWithOptions(sandboxed, sandboxRoot, {
containerWorkdir: sandbox.containerWorkdir,
})
: sandboxed,
];
}
const freshReadTool = createReadTool(workspaceRoot);
const wrapped = createOpenClawReadTool(freshReadTool, {
modelContextWindowTokens: options?.modelContextWindowTokens,
imageSanitization,
});
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
}
: undefined,
});
const processTool = createLazyProcessTool({
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
scopeKey,
});
if (tool.name === "bash" || tool.name === execToolName) {
return [];
}
if (tool.name === "write") {
if (sandboxRoot) {
return [];
}
const wrapped = createHostWorkspaceWriteTool(workspaceRoot, { workspaceOnly });
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
}
if (tool.name === "edit") {
if (sandboxRoot) {
return [];
}
const wrapped = createHostWorkspaceEditTool(workspaceRoot, { workspaceOnly });
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
}
return [tool];
})
: [];
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
const execTool = includeCoreTools
? createLazyExecTool({
...execDefaults,
host: options?.exec?.host ?? execConfig.host,
security: options?.exec?.security ?? execConfig.security,
ask: options?.exec?.ask ?? execConfig.ask,
trigger: options?.trigger,
node: options?.exec?.node ?? execConfig.node,
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
strictInlineEval: options?.exec?.strictInlineEval ?? execConfig.strictInlineEval,
safeBinTrustedDirs: options?.exec?.safeBinTrustedDirs ?? execConfig.safeBinTrustedDirs,
safeBinProfiles: options?.exec?.safeBinProfiles ?? execConfig.safeBinProfiles,
agentId,
cwd: workspaceRoot,
allowBackground,
scopeKey,
sessionKey: options?.sessionKey,
messageProvider: options?.messageProvider,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
accountId: options?.agentAccountId,
backgroundMs: options?.exec?.backgroundMs ?? execConfig.backgroundMs,
timeoutSec: options?.exec?.timeoutSec ?? execConfig.timeoutSec,
approvalRunningNoticeMs:
options?.exec?.approvalRunningNoticeMs ?? execConfig.approvalRunningNoticeMs,
notifyOnExit: options?.exec?.notifyOnExit ?? execConfig.notifyOnExit,
notifyOnExitEmptySuccess:
options?.exec?.notifyOnExitEmptySuccess ?? execConfig.notifyOnExitEmptySuccess,
sandbox: sandbox
? {
containerName: sandbox.containerName,
workspaceDir: sandbox.workspaceDir,
containerWorkdir: sandbox.containerWorkdir,
env: sandbox.backend?.env ?? sandbox.docker.env,
buildExecSpec: sandbox.backend?.buildExecSpec.bind(sandbox.backend),
finalizeExec: sandbox.backend?.finalizeExec?.bind(sandbox.backend),
}
: undefined,
})
: null;
const processTool = includeCoreTools
? createLazyProcessTool({
cleanupMs: cleanupMsOverride ?? execConfig.cleanupMs,
scopeKey,
})
: null;
const applyPatchTool =
!applyPatchEnabled || (sandboxRoot && !allowWorkspaceWrites)
? null
@@ -584,9 +594,53 @@ export function createOpenClawCodingTools(options?: {
: undefined,
workspaceOnly: applyPatchWorkspaceOnly,
});
const pluginToolAllowlist = collectExplicitAllowlist([
profilePolicy,
providerProfilePolicy,
globalPolicy,
globalProviderPolicy,
agentPolicy,
agentProviderPolicy,
groupPolicy,
sandboxToolPolicy,
subagentPolicy,
options?.runtimeToolAllowlist ? { allow: options.runtimeToolAllowlist } : undefined,
]);
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,
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,
...(sandboxRoot
...(includeCoreTools && sandboxRoot
? allowWorkspaceWrites
? [
workspaceOnly
@@ -610,65 +664,56 @@ export function createOpenClawCodingTools(options?: {
]
: []
: []),
...(applyPatchTool ? [applyPatchTool as unknown as AnyAgentTool] : []),
execTool as unknown as AnyAgentTool,
processTool as unknown as AnyAgentTool,
...(includeCoreTools && 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.).
...listChannelAgentTools({ cfg: options?.config }),
...createOpenClawTools({
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
agentSessionKey: options?.sessionKey,
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
agentAccountId: options?.agentAccountId,
agentTo: options?.messageTo,
agentThreadId: options?.messageThreadId,
agentGroupId: options?.groupId ?? null,
agentGroupChannel: options?.groupChannel ?? null,
agentGroupSpace: options?.groupSpace ?? null,
agentMemberRoleIds: options?.memberRoleIds,
agentDir: options?.agentDir,
sandboxRoot,
sandboxContainerWorkdir: sandbox?.containerWorkdir,
sandboxFsBridge,
fsPolicy,
workspaceDir: workspaceRoot,
spawnWorkspaceDir: options?.spawnWorkspaceDir
? resolveWorkspaceRoot(options.spawnWorkspaceDir)
: undefined,
sandboxed: !!sandbox,
config: options?.config,
pluginToolAllowlist: collectExplicitAllowlist([
profilePolicy,
providerProfilePolicy,
globalPolicy,
globalProviderPolicy,
agentPolicy,
agentProviderPolicy,
groupPolicy,
sandboxToolPolicy,
subagentPolicy,
options?.runtimeToolAllowlist ? { allow: options.runtimeToolAllowlist } : undefined,
]),
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
currentMessageId: options?.currentMessageId,
modelProvider: options?.modelProvider,
modelId: options?.modelId,
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
modelHasVision: options?.modelHasVision,
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
disableMessageTool: options?.disableMessageTool,
enableHeartbeatTool,
...(cronSelfRemoveOnlyJobId ? { cronSelfRemoveOnlyJobId } : {}),
requesterAgentIdOverride: agentId,
requesterSenderId: options?.senderId,
senderIsOwner: options?.senderIsOwner,
sessionId: options?.sessionId,
onYield: options?.onYield,
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
}),
...(includeCoreTools ? listChannelAgentTools({ cfg: options?.config }) : []),
...(includeCoreTools
? createOpenClawTools({
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
agentSessionKey: options?.sessionKey,
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
agentAccountId: options?.agentAccountId,
agentTo: options?.messageTo,
agentThreadId: options?.messageThreadId,
agentGroupId: options?.groupId ?? null,
agentGroupChannel: options?.groupChannel ?? null,
agentGroupSpace: options?.groupSpace ?? null,
agentMemberRoleIds: options?.memberRoleIds,
agentDir: options?.agentDir,
sandboxRoot,
sandboxContainerWorkdir: sandbox?.containerWorkdir,
sandboxFsBridge,
fsPolicy,
workspaceDir: workspaceRoot,
spawnWorkspaceDir: options?.spawnWorkspaceDir
? resolveWorkspaceRoot(options.spawnWorkspaceDir)
: undefined,
sandboxed: !!sandbox,
config: options?.config,
pluginToolAllowlist,
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
currentMessageId: options?.currentMessageId,
modelProvider: options?.modelProvider,
modelId: options?.modelId,
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
modelHasVision: options?.modelHasVision,
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
disableMessageTool: options?.disableMessageTool,
enableHeartbeatTool,
...(cronSelfRemoveOnlyJobId ? { cronSelfRemoveOnlyJobId } : {}),
requesterAgentIdOverride: agentId,
requesterSenderId: options?.senderId,
senderIsOwner: options?.senderIsOwner,
sessionId: options?.sessionId,
onYield: options?.onYield,
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
})
: pluginToolsOnly),
];
const toolsForMemoryFlush =
isMemoryFlushRun && memoryFlushWritePath