diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index e7b332711d..ccd4f94857 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -605,7 +605,7 @@ export async function loadCliConfig( process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true' || process.env['VITEST'] === 'true' ? false - : (settings.security?.folderTrust?.enabled ?? false); + : (settings.security?.folderTrust?.enabled ?? true); const trustedFolder = isWorkspaceTrusted(settings, cwd, { prompt: argv.prompt, diff --git a/packages/core/src/policy/policy-engine.ts b/packages/core/src/policy/policy-engine.ts index a3b9aa0992..63dcb1a9c5 100644 --- a/packages/core/src/policy/policy-engine.ts +++ b/packages/core/src/policy/policy-engine.ts @@ -543,13 +543,17 @@ export class PolicyEngine { let shellDirPath: string | undefined; const toolName = toolCall.name; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const args = toolCall.args as { command?: unknown; dir_path?: unknown }; - if (toolName && SHELL_TOOL_NAMES.includes(toolName)) { + if ( + toolName && + (SHELL_TOOL_NAMES.includes(toolName) || + (typeof args?.command === 'string' && args.command.trim().length > 0)) + ) { isShellCommand = true; - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const args = toolCall.args as { command?: string; dir_path?: string }; - command = args?.command; - shellDirPath = args?.dir_path; + command = args.command as string; + shellDirPath = typeof args.dir_path === 'string' ? args.dir_path : undefined; } // Find the first matching rule (already sorted by priority) @@ -645,7 +649,7 @@ export class PolicyEngine { debugLogger.debug( `[PolicyEngine.check] NO MATCH - using default decision: ${this.defaultDecision}`, ); - if (toolName && SHELL_TOOL_NAMES.includes(toolName)) { + if (isShellCommand && toolName) { let heuristicDecision = this.defaultDecision; if (!skipHeuristics && command) { heuristicDecision = await this.applyShellHeuristics( diff --git a/packages/core/src/tools/mcp-tool.test.ts b/packages/core/src/tools/mcp-tool.test.ts index 0a0b85d33f..cf13a973a7 100644 --- a/packages/core/src/tools/mcp-tool.test.ts +++ b/packages/core/src/tools/mcp-tool.test.ts @@ -252,7 +252,9 @@ describe('DiscoveredMCPTool', () => { mockToolSuccessResultObject, ); expect(toolResult.llmContent).toEqual([ - { text: stringifiedResponseContent }, + { + text: `\n${stringifiedResponseContent}\n`, + }, ]); expect(toolResult.returnDisplay).toBe(stringifiedResponseContent); }); @@ -435,7 +437,9 @@ describe('DiscoveredMCPTool', () => { mockToolSuccessResultObject, ); expect(toolResult.llmContent).toEqual([ - { text: stringifiedResponseContent }, + { + text: `\n${stringifiedResponseContent}\n`, + }, ]); expect(toolResult.returnDisplay).toBe(stringifiedResponseContent); }, @@ -456,7 +460,11 @@ describe('DiscoveredMCPTool', () => { abortSignal: new AbortController().signal, }); // 1. Assert that the llmContent sent to the scheduler is a clean Part array. - expect(toolResult.llmContent).toEqual([{ text: successMessage }]); + expect(toolResult.llmContent).toEqual([ + { + text: `\n${successMessage}\n`, + }, + ]); // 2. Assert that the display output is the simple text message. expect(toolResult.returnDisplay).toBe(successMessage); @@ -482,7 +490,7 @@ describe('DiscoveredMCPTool', () => { ); const invocation = tool.build(params); - const toolResult = await invocation.execute({ + const toolResult: ToolResult = await invocation.execute({ abortSignal: new AbortController().signal, }); expect(toolResult.llmContent).toEqual([ @@ -515,12 +523,12 @@ describe('DiscoveredMCPTool', () => { ); const invocation = tool.build(params); - const toolResult = await invocation.execute({ + const toolResult: ToolResult = await invocation.execute({ abortSignal: new AbortController().signal, }); expect(toolResult.llmContent).toEqual([ { - text: 'Resource Link: My Resource at file:///path/to/thing', + text: `[Tool '${serverToolName}' provided a resource link: My Resource at file:///path/to/thing]`, }, ]); expect(toolResult.returnDisplay).toBe( @@ -550,7 +558,9 @@ describe('DiscoveredMCPTool', () => { abortSignal: new AbortController().signal, }); expect(toolResult.llmContent).toEqual([ - { text: 'This is the text content.' }, + { + text: `\nThis is the text content.\n`, + }, ]); expect(toolResult.returnDisplay).toBe('This is the text content.'); }); @@ -573,7 +583,7 @@ describe('DiscoveredMCPTool', () => { ); const invocation = tool.build(params); - const toolResult = await invocation.execute({ + const toolResult: ToolResult = await invocation.execute({ abortSignal: new AbortController().signal, }); expect(toolResult.llmContent).toEqual([ @@ -609,11 +619,13 @@ describe('DiscoveredMCPTool', () => { ); const invocation = tool.build(params); - const toolResult = await invocation.execute({ + const toolResult: ToolResult = await invocation.execute({ abortSignal: new AbortController().signal, }); expect(toolResult.llmContent).toEqual([ - { text: 'First part.' }, + { + text: `\nFirst part.\n`, + }, { text: `[Tool '${serverToolName}' provided the following image data with mime-type: image/jpeg]`, }, @@ -623,7 +635,9 @@ describe('DiscoveredMCPTool', () => { data: 'BASE64_IMAGE_DATA', }, }, - { text: 'Second part.' }, + { + text: `\nSecond part.\n`, + }, ]); expect(toolResult.returnDisplay).toBe( 'First part.\n[Image: image/jpeg]\nSecond part.', @@ -645,7 +659,11 @@ describe('DiscoveredMCPTool', () => { const toolResult = await invocation.execute({ abortSignal: new AbortController().signal, }); - expect(toolResult.llmContent).toEqual([{ text: 'Valid part.' }]); + expect(toolResult.llmContent).toEqual([ + { + text: `\nValid part.\n`, + }, + ]); expect(toolResult.returnDisplay).toBe( 'Valid part.\n[Unknown content type: future_block]', ); @@ -681,15 +699,19 @@ describe('DiscoveredMCPTool', () => { ); const invocation = tool.build(params); - const toolResult = await invocation.execute({ + const toolResult: ToolResult = await invocation.execute({ abortSignal: new AbortController().signal, }); expect(toolResult.llmContent).toEqual([ - { text: 'Here is a resource.' }, { - text: 'Resource Link: My Resource at file:///path/to/resource', + text: `\nHere is a resource.\n`, + }, + { + text: `[Tool '${serverToolName}' provided a resource link: My Resource at file:///path/to/resource]`, + }, + { + text: `\nEmbedded text content.\n`, }, - { text: 'Embedded text content.' }, { text: `[Tool '${serverToolName}' provided the following image data with mime-type: image/jpeg]`, }, @@ -771,7 +793,11 @@ describe('DiscoveredMCPTool', () => { abortSignal: controller.signal, }); - expect(result.llmContent).toEqual([{ text: 'Success' }]); + expect(result.llmContent).toEqual([ + { + text: `\nSuccess\n`, + }, + ]); expect(result.returnDisplay).toBe('Success'); expect(mockCallTool).toHaveBeenCalledWith([ { name: serverToolName, args: params }, diff --git a/packages/core/src/tools/mcp-tool.ts b/packages/core/src/tools/mcp-tool.ts index caaba717d1..be2dbbc858 100644 --- a/packages/core/src/tools/mcp-tool.ts +++ b/packages/core/src/tools/mcp-tool.ts @@ -132,6 +132,7 @@ type McpMediaBlock = { type McpResourceBlock = { type: 'resource'; resource: { + uri?: string; text?: string; blob?: string; mimeType?: string; @@ -447,8 +448,23 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool< } } -function transformTextBlock(block: McpTextBlock): Part { - return { text: block.text }; +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function escapeAttribute(text: string): string { + return escapeHtml(text).replace(/"/g, '"').replace(/'/g, '''); +} + +function transformTextBlock(block: McpTextBlock, toolName: string): Part { + return { + text: `\n${escapeHtml( + block.text, + )}\n`, + }; } function transformImageAudioBlock( @@ -476,7 +492,13 @@ function transformResourceBlock( ): Part | Part[] | null { const resource = block.resource; if (resource?.text) { - return { text: resource.text }; + return { + text: `\n${escapeHtml(resource.text)}\n`, + }; } if (resource?.blob) { const mimeType = resource.mimeType || 'application/octet-stream'; @@ -495,9 +517,14 @@ function transformResourceBlock( return null; } -function transformResourceLinkBlock(block: McpResourceLinkBlock): Part { +function transformResourceLinkBlock( + block: McpResourceLinkBlock, + toolName: string, +): Part { return { - text: `Resource Link: ${block.title || block.name} at ${block.uri}`, + text: `[Tool '${toolName}' provided a resource link: ${ + block.title || block.name + } at ${block.uri}]`, }; } @@ -521,14 +548,14 @@ function transformMcpContentToParts(sdkResponse: Part[]): Part[] { (block: McpContentBlock): Part | Part[] | null => { switch (block.type) { case 'text': - return transformTextBlock(block); + return transformTextBlock(block, toolName); case 'image': case 'audio': return transformImageAudioBlock(block, toolName); case 'resource': return transformResourceBlock(block, toolName); case 'resource_link': - return transformResourceLinkBlock(block); + return transformResourceLinkBlock(block, toolName); default: return null; }