Skip to content

Commit 8f0a4bb

Browse files
dungdong-awslaileni-aws
authored andcommitted
feat(amazonq): add in-context permission update for built-in tools (#2062)
* fix(amazonq): add warning to executeBash regardless perm;fix optionValue in perm fill with all tools * fix: retain built-in tool permissions; bump up mynah-ui version * fix(chat-client): add warning to shell chat card even auto-accept * feat(amazonq): add permission for built-in tools metric to AgenticChatWithToolUse * feat(amazonq): add telemtry when built-in tool permission update * feat: add tool permission update in chat item card * feat: add hyperlink in description of in-context permission card * feat(chat-client): fix in-context dropdown * fix(chat-client): add missing type to render the dropdown * fix(amazonq): rename dropdown to quickSettings to be more semantic * feat(chat-client): add quickSettings to mcp approval chat card; clean up * feat(amazonq): re-name auto-run-command to trust-command for pairty with Kiro * chore: refactor logic * fix(chat-client): fix description hyperlink in permission quickSettings; clean up * refactor: clean up code * fix(chat-client): update text to align with mock * feat(chat-client): add in-context permission update to Mcp tools * feat(amazonq): add auto-approve back * fix: agentPath in processPermissionUpdates is undefined for builtInTools * fix(amazonq): remove in-context for mcp
1 parent 18e0415 commit 8f0a4bb

File tree

5 files changed

+116
-51
lines changed

5 files changed

+116
-51
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ import { CancellationError, workspaceUtils } from '@aws/lsp-core'
166166
import { FsRead, FsReadParams } from './tools/fsRead'
167167
import { ListDirectory, ListDirectoryParams } from './tools/listDirectory'
168168
import { FsWrite, FsWriteParams } from './tools/fsWrite'
169-
import { ExecuteBash, ExecuteBashParams } from './tools/executeBash'
169+
import { commandCategories, ExecuteBash, ExecuteBashParams } from './tools/executeBash'
170170
import { ExplanatoryParams, InvokeOutput, ToolApprovalException } from './tools/toolShared'
171171
import { validatePathBasic, validatePathExists, validatePaths as validatePathsSync } from './utils/pathValidation'
172172
import { GrepSearch, SanitizedRipgrepOutput } from './tools/grepSearch'
@@ -233,7 +233,7 @@ import { getLatestAvailableModel } from './utils/agenticChatControllerHelper'
233233
import { ActiveUserTracker } from '../../shared/activeUserTracker'
234234
import { UserContext } from '../../client/token/codewhispererbearertokenclient'
235235
import { CodeWhispererServiceToken } from '../../shared/codeWhispererService'
236-
import { McpPermissionType } from './tools/mcp/mcpTypes'
236+
import { McpPermissionType, MCPServerPermission } from './tools/mcp/mcpTypes'
237237
import { DisplayFindings } from './tools/qCodeAnalysis/displayFindings'
238238
import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager'
239239

@@ -407,24 +407,23 @@ export class AgenticChatController implements ChatHandlers {
407407
if (params.buttonId === BUTTON_TRUST_COMMAND) {
408408
// get result from metadata
409409
const toolName = params.metadata!['toolName']
410-
const newPermission = params.metadata!['permission']
410+
const new_permission = params.metadata!['permission']
411411
const serverName = params.metadata!['serverName']
412412

413-
const currentPermission = McpManager.instance.getToolPerm(serverName, toolName)
413+
const current_permission = McpManager.instance.getToolPerm(serverName, toolName)
414414
// only trigger update if curren != previous
415-
if (currentPermission !== newPermission) {
415+
if (current_permission !== new_permission) {
416416
// generate perm object
417417
const perm = await this.#mcpEventHandler.generateEmptyBuiltInToolPermission()
418418

419419
// load updated permission
420-
perm.toolPerms[toolName] = newPermission as McpPermissionType
420+
perm.toolPerms[toolName] = new_permission as McpPermissionType
421421

422422
// update permission
423423
try {
424424
await McpManager.instance.updateServerPermission(serverName, perm)
425425
// if the new permission is asks --> only update permission, dont continue
426-
// this only happen on a completed card
427-
if (newPermission === 'ask') {
426+
if (new_permission === 'ask') {
428427
return {
429428
success: true,
430429
}
@@ -449,7 +448,6 @@ export class AgenticChatController implements ChatHandlers {
449448
params.messageId.endsWith(SUFFIX_PERMISSION)
450449
? params.messageId.replace(SUFFIX_PERMISSION, '')
451450
: params.messageId
452-
453451
const handler = session.data.getDeferredToolExecution(messageId)
454452
if (!handler?.reject || !handler.resolve) {
455453
if (params.buttonId === BUTTON_TRUST_COMMAND) {
@@ -1460,6 +1458,18 @@ export class AgenticChatController implements ChatHandlers {
14601458
metric.setDimension('requestIds', metric.metric.requestIds)
14611459
const toolNames = this.#toolUseLatencies.map(item => item.toolName)
14621460
const toolUseIds = this.#toolUseLatencies.map(item => item.toolUseId)
1461+
1462+
const builtInToolNames = new Set(this.#features.agent.getBuiltInToolNames())
1463+
const permission: string[] = []
1464+
1465+
for (const toolName of toolNames) {
1466+
if (builtInToolNames.has(toolName)) {
1467+
permission.push(McpManager.instance.getToolPerm('Built-in', toolName))
1468+
} else {
1469+
// TODO: determine mcp-server of the current tool to get permission
1470+
}
1471+
}
1472+
14631473
this.#telemetryController.emitAgencticLoop_InvokeLLM(
14641474
response.$metadata.requestId!,
14651475
conversationId,
@@ -1475,7 +1485,8 @@ export class AgenticChatController implements ChatHandlers {
14751485
this.#timeBetweenChunks,
14761486
session.pairProgrammingMode,
14771487
this.#abTestingAllocation?.experimentName,
1478-
this.#abTestingAllocation?.userVariation
1488+
this.#abTestingAllocation?.userVariation,
1489+
permission
14791490
)
14801491
} else {
14811492
// Send an error card to UI?
@@ -1708,6 +1719,7 @@ export class AgenticChatController implements ChatHandlers {
17081719
promptBlockId: number,
17091720
session: ChatSessionService,
17101721
toolName: string,
1722+
commandCategory?: CommandCategory,
17111723
tabId?: string
17121724
) {
17131725
const deferred = this.#createDeferred()
@@ -1716,7 +1728,7 @@ export class AgenticChatController implements ChatHandlers {
17161728
await deferred.promise
17171729
// Note: we want to overwrite the button block because it already exists in the stream.
17181730
await resultStream.overwriteResultBlock(
1719-
this.#getUpdateToolConfirmResult(toolUse, true, toolName, tabId),
1731+
this.#getUpdateToolConfirmResult(toolUse, true, toolName, undefined, commandCategory, tabId),
17201732
promptBlockId
17211733
)
17221734
}
@@ -1772,6 +1784,10 @@ export class AgenticChatController implements ChatHandlers {
17721784
})
17731785
}
17741786
}
1787+
1788+
// for later use
1789+
let finalCommandCategory: CommandCategory | undefined
1790+
17751791
switch (toolUse.name) {
17761792
case FS_READ:
17771793
case LIST_DIRECTORY:
@@ -1807,6 +1823,8 @@ export class AgenticChatController implements ChatHandlers {
18071823
approvedPaths
18081824
)
18091825

1826+
finalCommandCategory = commandCategory
1827+
18101828
const isExecuteBash = toolUse.name === EXECUTE_BASH
18111829

18121830
// check if tool execution's path is out of workspace
@@ -1837,7 +1855,7 @@ export class AgenticChatController implements ChatHandlers {
18371855
warning,
18381856
commandCategory,
18391857
toolUse.name,
1840-
undefined,
1858+
builtInPermission,
18411859
tabId
18421860
)
18431861
cachedButtonBlockId = await chatResultStream.writeResultBlock(confirmationResult)
@@ -1859,6 +1877,7 @@ export class AgenticChatController implements ChatHandlers {
18591877
cachedButtonBlockId,
18601878
session,
18611879
toolUse.name,
1880+
commandCategory,
18621881
tabId
18631882
)
18641883
}
@@ -1913,7 +1932,7 @@ export class AgenticChatController implements ChatHandlers {
19131932
requiresAcceptance,
19141933
warning,
19151934
undefined,
1916-
toolName, // Pass the original tool name here
1935+
toolName, // Pass the original tool name here,
19171936
undefined,
19181937
tabId
19191938
)
@@ -1924,6 +1943,7 @@ export class AgenticChatController implements ChatHandlers {
19241943
cachedButtonBlockId,
19251944
session,
19261945
toolName,
1946+
undefined,
19271947
tabId
19281948
)
19291949
}
@@ -1976,7 +1996,7 @@ export class AgenticChatController implements ChatHandlers {
19761996
session.addApprovedPath(inputPath)
19771997
}
19781998

1979-
const ws = this.#getWritableStream(chatResultStream, toolUse)
1999+
const ws = this.#getWritableStream(chatResultStream, toolUse, finalCommandCategory)
19802000
const result = await this.#features.agent.runTool(toolUse.name, toolUse.input, token, ws)
19812001

19822002
let toolResultContent: ToolResultContentBlock
@@ -2394,7 +2414,11 @@ export class AgenticChatController implements ChatHandlers {
23942414
})
23952415
}
23962416

2397-
#getWritableStream(chatResultStream: AgenticChatResultStream, toolUse: ToolUse): WritableStream | undefined {
2417+
#getWritableStream(
2418+
chatResultStream: AgenticChatResultStream,
2419+
toolUse: ToolUse,
2420+
commandCategory?: CommandCategory
2421+
): WritableStream | undefined {
23982422
if (toolUse.name === CodeReview.toolName) {
23992423
return this.#getToolOverWritableStream(chatResultStream, toolUse)
24002424
}
@@ -2413,7 +2437,14 @@ export class AgenticChatController implements ChatHandlers {
24132437

24142438
const completedHeader: ChatMessage['header'] = {
24152439
body: 'shell',
2416-
status: { status: 'success', icon: 'ok', text: 'Completed' },
2440+
status: {
2441+
status: 'success',
2442+
icon: 'ok',
2443+
text: 'Completed',
2444+
...(toolUse.name === EXECUTE_BASH
2445+
? { description: this.#getCommandCategoryDescription(commandCategory ?? CommandCategory.ReadOnly) }
2446+
: {}),
2447+
},
24172448
buttons: [],
24182449
}
24192450

@@ -2465,10 +2496,11 @@ export class AgenticChatController implements ChatHandlers {
24652496
isAccept: boolean,
24662497
originalToolName: string,
24672498
toolType?: string,
2499+
commandCategory?: CommandCategory,
24682500
tabId?: string
24692501
): ChatResult {
24702502
const toolName = originalToolName ?? (toolType || toolUse.name)
2471-
2503+
const quickSettings = this.#buildQuickSettings(toolUse, toolName!, toolType, tabId)
24722504
// Handle bash commands with special formatting
24732505
if (toolName === EXECUTE_BASH) {
24742506
return {
@@ -2488,6 +2520,7 @@ export class AgenticChatController implements ChatHandlers {
24882520
}),
24892521
buttons: isAccept ? [this.#renderStopShellCommandButton()] : [],
24902522
},
2523+
quickSettings,
24912524
}
24922525
}
24932526

@@ -2775,13 +2808,8 @@ export class AgenticChatController implements ChatHandlers {
27752808
const quickSettings = this.#buildQuickSettings(toolUse, toolName!, toolType, tabId)
27762809
// Configure tool-specific UI elements
27772810
switch (toolName) {
2778-
case EXECUTE_BASH: {
2811+
case 'executeBash': {
27792812
const commandString = (toolUse.input as unknown as ExecuteBashParams).command
2780-
// get feature flag
2781-
const shortcut =
2782-
this.#features.lsp.getClientInitializeParams()?.initializationOptions?.aws?.awsClientCapabilities?.q
2783-
?.shortcut
2784-
27852813
const runKey = this.#getKeyBinding('aws.amazonq.runCmdExecution')
27862814
const rejectKey = this.#getKeyBinding('aws.amazonq.rejectCmdExecution')
27872815

@@ -2817,16 +2845,12 @@ export class AgenticChatController implements ChatHandlers {
28172845
: undefined
28182846

28192847
header = {
2820-
status: requiresAcceptance
2821-
? {
2822-
icon: statusIcon,
2823-
status: statusType,
2824-
position: 'left',
2825-
description: this.#getCommandCategoryDescription(
2826-
commandCategory ?? CommandCategory.ReadOnly
2827-
),
2828-
}
2829-
: {},
2848+
status: {
2849+
icon: statusIcon,
2850+
status: statusType,
2851+
position: 'left',
2852+
description: this.#getCommandCategoryDescription(commandCategory ?? CommandCategory.ReadOnly),
2853+
},
28302854
body: 'shell',
28312855
buttons,
28322856
}
@@ -2944,6 +2968,7 @@ export class AgenticChatController implements ChatHandlers {
29442968
messageId: this.#getMessageIdForToolUse(toolType, toolUse),
29452969
header,
29462970
body: warning ? (toolName === EXECUTE_BASH ? '' : '\n\n') + body : body,
2971+
quickSettings,
29472972
}
29482973
} else {
29492974
return {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { URI } from 'vscode-uri'
3434
interface PermissionOption {
3535
label: string
3636
value: string
37-
description?: string
37+
description: string
3838
}
3939

4040
export class McpEventHandler {
@@ -1165,7 +1165,7 @@ export class McpEventHandler {
11651165
case 'fsWrite':
11661166
case 'fsReplace':
11671167
return 'Create or edit files.'
1168-
case 'qCodeReview':
1168+
case 'codeReview':
11691169
return 'Review tool analyzes code for security vulnerabilities, quality issues, and best practices across multiple programming languages.'
11701170
default:
11711171
return ''
@@ -1178,12 +1178,6 @@ export class McpEventHandler {
11781178
async #handleMcpPermissionChange(params: McpServerClickParams) {
11791179
const serverName = params.title
11801180

1181-
// combine fsWrite and fsReplace into fsWrite
1182-
if (serverName === 'Built-in' && params.optionsValues?.fsWrite) {
1183-
// add fsReplace along
1184-
params.optionsValues.fsReplace = params.optionsValues.fsWrite
1185-
}
1186-
11871181
const updatedPermissionConfig = params.optionsValues
11881182
if (!serverName || !updatedPermissionConfig) {
11891183
return { id: params.id }
@@ -1258,6 +1252,33 @@ export class McpEventHandler {
12581252
transportType: transportType,
12591253
languageServerVersion: this.#features.runtime.serverInfo.version,
12601254
})
1255+
} else {
1256+
// it's mean built-in tool, but do another extra check to confirm
1257+
if (serverName === 'Built-in') {
1258+
let toolName: string[] = []
1259+
let perm: string[] = []
1260+
1261+
for (const [key, val] of Object.entries(permission.toolPerms)) {
1262+
toolName.push(key)
1263+
perm.push(val)
1264+
}
1265+
1266+
this.#telemetryController?.emitMCPServerInitializeEvent({
1267+
source: 'updatePermission',
1268+
command: 'Built-in',
1269+
enabled: true,
1270+
numTools: McpManager.instance.getAllToolsWithPermissions(serverName).length,
1271+
scope:
1272+
permission.__configPath__ ===
1273+
getGlobalAgentConfigPath(this.#features.workspace.fs.getUserHomeDir())
1274+
? 'global'
1275+
: 'workspace',
1276+
transportType: '',
1277+
languageServerVersion: this.#features.runtime.serverInfo.version,
1278+
toolName: toolName,
1279+
permission: perm,
1280+
})
1281+
}
12611282
}
12621283

12631284
// Clear the pending permission config after applying
@@ -1398,10 +1419,11 @@ export class McpEventHandler {
13981419
* Processes permission updates from the UI
13991420
*/
14001421
async #processPermissionUpdates(serverName: string, updatedPermissionConfig: any, agentPath: string | undefined) {
1422+
const builtInToolAgentPath = await this.#getAgentPath()
14011423
const perm: MCPServerPermission = {
14021424
enabled: true,
14031425
toolPerms: {},
1404-
__configPath__: agentPath,
1426+
__configPath__: serverName === 'Built-in' ? builtInToolAgentPath : agentPath,
14051427
}
14061428

14071429
// Process each tool permission setting

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ export class McpManager {
456456
*/
457457
public isToolDisabled(server: string, tool: string): boolean {
458458
// built-in tools cannot be disabled
459-
if (server === 'builtIn') {
459+
if (server === 'Built-in') {
460460
return false
461461
}
462462

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpUtils.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,8 @@ export function enabledMCP(params: InitializeParams | undefined): boolean {
628628
export function convertPersonaToAgent(
629629
persona: PersonaConfig,
630630
mcpServers: Record<string, MCPServerConfig>,
631-
featureAgent: Agent
631+
featureAgent: Agent,
632+
existingAgentConfig: AgentConfig | undefined
632633
): AgentConfig {
633634
const agent: AgentConfig = {
634635
name: 'default-agent',
@@ -695,12 +696,22 @@ export function convertPersonaToAgent(
695696
}
696697
}
697698

698-
// Add default allowed tools
699-
const writeToolNames = new Set(featureAgent.getBuiltInWriteToolNames())
699+
// handle permission of built-in tools
700+
// check if agent config exists
700701
const defaultAllowedTools = featureAgent.getBuiltInToolNames().filter(toolName => toolName !== EXECUTE_BASH)
701-
for (const toolName of defaultAllowedTools) {
702-
if (!agent.allowedTools.includes(toolName)) {
703-
agent.allowedTools.push(toolName)
702+
if (!existingAgentConfig) {
703+
// not yet created --> add all defaults tools
704+
for (const toolName of defaultAllowedTools) {
705+
if (!agent.allowedTools.includes(toolName)) {
706+
agent.allowedTools.push(toolName)
707+
}
708+
}
709+
} else {
710+
// only consider tools that are not in existingAgentConfig
711+
for (const toolName of defaultAllowedTools) {
712+
if (!agent.allowedTools.includes(toolName) && !existingAgentConfig.tools.includes(toolName)) {
713+
agent.allowedTools.push(toolName)
714+
}
704715
}
705716
}
706717

@@ -909,7 +920,8 @@ async function migrateConfigToAgent(
909920
}
910921

911922
// Convert to agent config
912-
const newAgentConfig = convertPersonaToAgent(personaConfig, serverConfigs, agent)
923+
logging.info('Migrating started')
924+
const newAgentConfig = convertPersonaToAgent(personaConfig, serverConfigs, agent, existingAgentConfig)
913925
newAgentConfig.includedFiles = ['AmazonQ.md', 'README.md', '.amazonq/rules/**/*.md']
914926
newAgentConfig.resources = [] // Initialize with empty array
915927

0 commit comments

Comments
 (0)