🛡️ 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 = {};