🛡️ fix: Gate Bash PTC Capabilities (#13053)

This commit is contained in:
Danny Avila 2026-05-10 21:23:02 -04:00 committed by GitHub
parent 030dc98a1d
commit 5bab22d236
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 213 additions and 15 deletions

View file

@ -546,6 +546,10 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
const areToolsEnabled = checkCapability(AgentCapabilities.tools);
const actionsEnabled = checkCapability(AgentCapabilities.actions);
const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools);
const programmaticToolsEnabled = enabledCapabilities.has(AgentCapabilities.programmatic_tools);
const codeExecutionEnabled =
agent.tools?.includes(Tools.execute_code) === true &&
enabledCapabilities.has(AgentCapabilities.execute_code);
const filteredTools = agent.tools?.filter((tool) => {
if (tool === Tools.file_search) {
@ -721,6 +725,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
tools: filteredTools,
toolOptions: agent.tool_options,
deferredToolsEnabled,
programmaticToolsEnabled,
codeExecutionEnabled,
},
{
isBuiltInTool,
@ -775,6 +781,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
tools: filteredTools,
toolOptions: agent.tool_options,
deferredToolsEnabled,
programmaticToolsEnabled,
codeExecutionEnabled,
},
{
isBuiltInTool,
@ -1003,6 +1011,10 @@ async function loadAgentTools({
/** Build tool registry from MCP tools and create PTC/tool search tools if configured */
const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools);
const programmaticToolsEnabled = enabledCapabilities.has(AgentCapabilities.programmatic_tools);
const codeExecutionEnabled =
agent.tools?.includes(Tools.execute_code) === true &&
enabledCapabilities.has(AgentCapabilities.execute_code);
const { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools } =
await buildToolClassification({
loadedTools,
@ -1010,6 +1022,8 @@ async function loadAgentTools({
agentId: agent.id,
agentToolOptions: agent.tool_options,
deferredToolsEnabled,
programmaticToolsEnabled,
codeExecutionEnabled,
authHeaders: () => getCodeApiAuthHeaders(req),
});
@ -1256,17 +1270,26 @@ async function loadToolsForExecution({
const allLoadedTools = [];
const configurable = { userMCPAuthMap };
if (actionsEnabled === undefined) {
const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id);
actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions);
}
const isToolSearch = toolNames.includes(AgentConstants.TOOL_SEARCH);
const ptcToolNames = [
AgentConstants.BASH_PROGRAMMATIC_TOOL_CALLING,
AgentConstants.PROGRAMMATIC_TOOL_CALLING,
].filter((name) => toolNames.includes(name));
const isPTC = ptcToolNames.length > 0;
const isPTCRequested = ptcToolNames.length > 0;
let enabledCapabilities;
if (actionsEnabled === undefined || isPTCRequested) {
enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id);
}
if (actionsEnabled === undefined) {
actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions);
}
const isPTC =
isPTCRequested &&
enabledCapabilities.has(AgentCapabilities.programmatic_tools) &&
enabledCapabilities.has(AgentCapabilities.execute_code) &&
agent?.tools?.includes(Tools.execute_code) === true;
logger.debug(
`[loadToolsForExecution] isToolSearch: ${isToolSearch}, toolRegistry: ${toolRegistry?.size ?? 'undefined'}`,

View file

@ -300,14 +300,20 @@ describe('ToolService - Action Capability Gating', () => {
const actionToolName = `get_weather${actionDelimiter}api_example_com`;
const regularTool = Tools.web_search;
it('loads bash PTC under the legacy programmatic tool name for event-driven compatibility', async () => {
const req = createMockReq([]);
it('loads bash PTC under the legacy programmatic tool name when code capabilities are enabled', async () => {
const capabilities = [
AgentCapabilities.tools,
AgentCapabilities.programmatic_tools,
AgentCapabilities.execute_code,
];
const req = createMockReq(capabilities);
const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
const result = await loadToolsForExecution({
req,
res: {},
agent: { id: 'agent_ptc' },
agent: { id: 'agent_ptc', tools: [Tools.execute_code] },
toolNames: [Constants.PROGRAMMATIC_TOOL_CALLING],
toolRegistry,
actionsEnabled: false,
@ -320,6 +326,50 @@ describe('ToolService - Action Capability Gating', () => {
expect(result.configurable.ptcToolMap.size).toBe(0);
});
it('does not load PTC when programmatic tools capability is disabled', async () => {
const capabilities = [AgentCapabilities.tools, AgentCapabilities.execute_code];
const req = createMockReq(capabilities);
const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
const result = await loadToolsForExecution({
req,
res: {},
agent: { id: 'agent_ptc', tools: [Tools.execute_code] },
toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING],
toolRegistry,
actionsEnabled: false,
});
expect(result.loadedTools.map((tool) => tool.name)).toEqual([]);
expect(result.configurable.toolRegistry).toBeUndefined();
expect(result.configurable.ptcToolMap).toBeUndefined();
});
it('does not load PTC when agent did not request execute_code', async () => {
const capabilities = [
AgentCapabilities.tools,
AgentCapabilities.programmatic_tools,
AgentCapabilities.execute_code,
];
const req = createMockReq(capabilities);
const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
const result = await loadToolsForExecution({
req,
res: {},
agent: { id: 'agent_ptc', tools: [] },
toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING],
toolRegistry,
actionsEnabled: false,
});
expect(result.loadedTools.map((tool) => tool.name)).toEqual([]);
expect(result.configurable.toolRegistry).toBeUndefined();
expect(result.configurable.ptcToolMap).toBeUndefined();
});
it('should skip action tool loading when actionsEnabled=false', async () => {
const req = createMockReq([]);
req.config = {};

View file

@ -308,6 +308,8 @@ describe('classification.ts', () => {
agentId: 'agent1',
agentToolOptions,
deferredToolsEnabled: true,
programmaticToolsEnabled: true,
codeExecutionEnabled: true,
definitionsOnly: true,
});
@ -327,6 +329,8 @@ describe('classification.ts', () => {
agentId: 'agent1',
agentToolOptions,
deferredToolsEnabled: true,
programmaticToolsEnabled: true,
codeExecutionEnabled: true,
definitionsOnly: true,
});
@ -334,7 +338,7 @@ describe('classification.ts', () => {
expect(result.toolRegistry?.has('tool_search')).toBe(true);
});
it('should still add PTC definition when definitionsOnly=true and has programmatic tools', async () => {
it('should add PTC definition when definitionsOnly=true and capabilities allow programmatic tools', async () => {
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
const agentToolOptions: AgentToolOptions = {
@ -347,6 +351,8 @@ describe('classification.ts', () => {
agentId: 'agent1',
agentToolOptions,
deferredToolsEnabled: true,
programmaticToolsEnabled: true,
codeExecutionEnabled: true,
definitionsOnly: true,
});
@ -355,7 +361,7 @@ describe('classification.ts', () => {
expect(result.additionalTools.length).toBe(0);
});
it('should create bash PTC tool when agent has programmatic tools', async () => {
it('should create bash PTC tool when capabilities allow programmatic tools', async () => {
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
const agentToolOptions: AgentToolOptions = {
@ -367,6 +373,8 @@ describe('classification.ts', () => {
userId: 'user1',
agentId: 'agent1',
agentToolOptions,
programmaticToolsEnabled: true,
codeExecutionEnabled: true,
});
expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(true);
@ -374,6 +382,42 @@ describe('classification.ts', () => {
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(true);
});
it('should not add PTC when programmatic tools capability is disabled', async () => {
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
const result = await buildToolClassification({
loadedTools,
userId: 'user1',
agentId: 'agent1',
agentToolOptions: {
tool1: { allowed_callers: ['code_execution'] },
},
codeExecutionEnabled: true,
});
expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(false);
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false);
expect(result.toolRegistry?.has('run_tools_with_bash')).toBe(false);
});
it('should not add PTC when code execution is not enabled for the agent', async () => {
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
const result = await buildToolClassification({
loadedTools,
userId: 'user1',
agentId: 'agent1',
agentToolOptions: {
tool1: { allowed_callers: ['code_execution'] },
},
programmaticToolsEnabled: true,
});
expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(false);
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false);
expect(result.toolRegistry?.has('run_tools_with_bash')).toBe(false);
});
it('should create tool instances when definitionsOnly=false (default)', async () => {
const loadedTools: GenericTool[] = [createMCPTool('tool1')];

View file

@ -185,6 +185,10 @@ export interface BuildToolClassificationParams {
agentToolOptions?: AgentToolOptions;
/** Whether the deferred_tools capability is enabled (from agent config) */
deferredToolsEnabled?: boolean;
/** Whether the programmatic_tools capability is enabled (from agent config) */
programmaticToolsEnabled?: boolean;
/** Whether code execution is enabled and requested by this agent */
codeExecutionEnabled?: boolean;
/** When true, skip creating tool instances (for event-driven mode) */
definitionsOnly?: boolean;
/** Optional host-supplied Code API auth headers for remote programmatic execution. */
@ -238,7 +242,7 @@ export function agentHasDeferredTools(toolRegistry: LCToolRegistry): boolean {
* 1. Filters loaded tools for MCP tools
* 2. Extracts tool definitions and builds the registry from agent's tool_options
* 3. Cleans up temporary mcpJsonSchema properties
* 4. Creates PTC tool only if agent has tools configured for programmatic calling
* 4. Creates PTC tool only if capabilities allow the agent's programmatic tools
* 5. Creates tool search tool only if agent has deferred tools
*
* @param params - Parameters including loaded tools, userId, agentId, agentToolOptions, and dependencies
@ -253,6 +257,8 @@ export async function buildToolClassification(
agentToolOptions,
definitionsOnly = false,
deferredToolsEnabled = true,
programmaticToolsEnabled = false,
codeExecutionEnabled = false,
authHeaders,
} = params;
const additionalTools: GenericTool[] = [];
@ -275,10 +281,11 @@ export async function buildToolClassification(
/**
* Check if this agent actually has tools configured for these features.
* Only enable PTC if the agent has programmatic tools.
* Only enable PTC if code/programmatic capabilities allow the agent's programmatic tools.
* Only enable tool search if the agent has deferred tools AND the capability is enabled.
*/
const hasProgrammaticTools = agentHasProgrammaticTools(toolRegistry);
const hasProgrammaticTools =
programmaticToolsEnabled && codeExecutionEnabled && agentHasProgrammaticTools(toolRegistry);
const hasDeferredTools = deferredToolsEnabled && agentHasDeferredTools(toolRegistry);
/** Clear defer_loading if capability disabled */

View file

@ -309,6 +309,66 @@ describe('definitions.ts', () => {
});
});
describe('programmatic tool calling capability gate', () => {
const mcpToolName = 'run_report_mcp_server_one';
const serverTools = {
[mcpToolName]: {
function: {
name: mcpToolName,
description: 'Run report',
parameters: { type: 'object', properties: {} },
},
},
};
it('does not add Bash PTC definitions unless both code and programmatic capabilities are enabled', async () => {
mockGetOrFetchMCPServerTools.mockResolvedValueOnce(serverTools);
const result = await loadToolDefinitions(
{
userId: 'user-123',
agentId: 'agent-123',
tools: [mcpToolName],
toolOptions: {
[mcpToolName]: { allowed_callers: ['code_execution'] },
},
codeExecutionEnabled: true,
},
{
getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools,
isBuiltInTool: mockIsBuiltInTool,
},
);
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false);
expect(result.toolRegistry.has('run_tools_with_bash')).toBe(false);
});
it('adds Bash PTC definitions when code and programmatic capabilities are enabled', async () => {
mockGetOrFetchMCPServerTools.mockResolvedValueOnce(serverTools);
const result = await loadToolDefinitions(
{
userId: 'user-123',
agentId: 'agent-123',
tools: [mcpToolName],
toolOptions: {
[mcpToolName]: { allowed_callers: ['code_execution'] },
},
programmaticToolsEnabled: true,
codeExecutionEnabled: true,
},
{
getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools,
isBuiltInTool: mockIsBuiltInTool,
},
);
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(true);
expect(result.toolRegistry.has('run_tools_with_bash')).toBe(true);
});
});
describe('MCP tool definitions with server name variants', () => {
it('should load MCP tools with underscored server names (server_one)', async () => {
const mockServerTools = {

View file

@ -35,6 +35,10 @@ export interface LoadToolDefinitionsParams {
toolOptions?: AgentToolOptions;
/** Whether deferred tools feature is enabled */
deferredToolsEnabled?: boolean;
/** Whether programmatic tool calling is enabled */
programmaticToolsEnabled?: boolean;
/** Whether code execution is enabled and requested by this agent */
codeExecutionEnabled?: boolean;
}
export interface ActionToolDefinition {
@ -71,7 +75,15 @@ export async function loadToolDefinitions(
params: LoadToolDefinitionsParams,
deps: LoadToolDefinitionsDeps,
): Promise<LoadToolDefinitionsResult> {
const { userId, agentId, tools, toolOptions = {}, deferredToolsEnabled = false } = params;
const {
userId,
agentId,
tools,
toolOptions = {},
deferredToolsEnabled = false,
programmaticToolsEnabled = false,
codeExecutionEnabled = false,
} = params;
const { getOrFetchMCPServerTools, isBuiltInTool, getActionToolDefinitions } = deps;
const emptyResult: LoadToolDefinitionsResult = {
@ -191,6 +203,8 @@ export async function loadToolDefinitions(
agentId,
loadedTools,
deferredToolsEnabled,
programmaticToolsEnabled,
codeExecutionEnabled,
definitionsOnly: true,
agentToolOptions: toolOptions,
});