Skip to content

Commit 741c2c4

Browse files
authored
fix(chat): Adding conditional check for Read and List tool execution (aws#7026)
## Problem - Reading Tool and List Directory tool requires users acceptance if IDE wants to perform operation outside of workspace. ## Solution - This problem is fixed as part of this PR ![image](https://github.com/user-attachments/assets/ad2811a6-7dca-4f21-b1f5-4c7809ec58e9) --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 7817f35 commit 741c2c4

File tree

7 files changed

+70
-26
lines changed

7 files changed

+70
-26
lines changed

packages/core/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@
459459
"AWS.amazonq.executeBash.reject": "Reject",
460460
"AWS.amazonq.chat.directive.pairProgrammingModeOn": "You are using **pair programming mode**: Q can now list files, preview code diffs and allow you to run shell commands.",
461461
"AWS.amazonq.chat.directive.pairProgrammingModeOff": "You turned off **pair programming mode**. Q will not include code diffs or run commands in the chat.",
462+
"AWS.amazonq.chat.directive.permission.readAndList": "I need permission to read files and list directories outside the workspace.",
462463
"AWS.amazonq.chat.directive.runCommandToProceed": "Run the command to proceed.",
463464
"AWS.toolkit.lambda.walkthrough.quickpickTitle": "Application Builder Walkthrough",
464465
"AWS.toolkit.lambda.walkthrough.title": "Get started building your application",

packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,11 @@ export class Messenger {
376376
changeList,
377377
explanation
378378
)
379-
await ToolUtils.queueDescription(tool, chatStream)
379+
await ToolUtils.queueDescription(
380+
tool,
381+
chatStream,
382+
chatStream.validation.requiresAcceptance
383+
)
380384
if (session.messageIdToUpdate === undefined && tool.type === ToolType.FsRead) {
381385
// Store the first messageId in a chain of tool uses
382386
session.setMessageIdToUpdate(toolUse.toolUseId)
@@ -748,13 +752,17 @@ export class Messenger {
748752
}
749753

750754
// Handle read tool and list directory messages
751-
if (toolUse?.name === ToolType.FsRead || toolUse?.name === ToolType.ListDirectory) {
755+
if (
756+
(toolUse?.name === ToolType.FsRead || toolUse?.name === ToolType.ListDirectory) &&
757+
!validation.requiresAcceptance
758+
) {
752759
return this.sendReadAndListDirToolMessage(toolUse, session, tabID, triggerID, messageIdToUpdate)
753760
}
754761

755762
// Handle file write tool, execute bash tool and bash command output log messages
756763
const buttons: ChatItemButton[] = []
757764
let header: ChatItemHeader | undefined = undefined
765+
let messageID: string = toolUse?.toolUseId ?? ''
758766
if (toolUse?.name === ToolType.ExecuteBash && message.startsWith('```shell')) {
759767
if (validation.requiresAcceptance) {
760768
const buttons: ChatItemButton[] = [
@@ -825,6 +833,10 @@ export class Messenger {
825833
}
826834
} else if (toolUse?.name === ToolType.ListDirectory || toolUse?.name === ToolType.FsRead) {
827835
if (validation.requiresAcceptance) {
836+
/** For Read and List Directory tools
837+
* requiredAcceptance = false, we use messageID = toolID and we keep on updating this messageID
838+
* requiredAcceptance = true, IDE sends messageID != toolID, some default value, as this overlaps with previous message. */
839+
messageID = 'toolUse'
828840
const buttons: ChatItemButton[] = [
829841
{
830842
id: 'confirm-tool-use',
@@ -840,7 +852,6 @@ export class Messenger {
840852
},
841853
]
842854
header = {
843-
body: 'shell',
844855
buttons,
845856
}
846857
}
@@ -859,7 +870,7 @@ export class Messenger {
859870
followUpsHeader: undefined,
860871
relatedSuggestions: undefined,
861872
triggerID,
862-
messageID: toolUse?.toolUseId ?? '',
873+
messageID,
863874
userIntent: undefined,
864875
codeBlockLanguage: undefined,
865876
contextList: undefined,

packages/core/src/codewhispererChat/tools/chatStream.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class ChatStream extends Writable {
2929
private readonly messageIdToUpdate: string | undefined,
3030
// emitEvent decides to show the streaming message or read/list directory tool message to the user.
3131
private readonly emitEvent: boolean,
32-
private readonly validation: CommandValidation,
32+
readonly validation: CommandValidation,
3333
private readonly isReadorList: boolean,
3434
private readonly changeList?: Change[],
3535
private readonly explanation?: string,
@@ -45,7 +45,13 @@ export class ChatStream extends Writable {
4545
if (this.explanation) {
4646
this.messenger.sendDirectiveMessage(tabID, triggerID, this.explanation)
4747
}
48-
if (validation.requiresAcceptance && this.toolUse?.name === 'executeBash') {
48+
if (validation.requiresAcceptance && isReadorList) {
49+
this.messenger.sendDirectiveMessage(
50+
tabID,
51+
triggerID,
52+
i18n('AWS.amazonq.chat.directive.permission.readAndList')
53+
)
54+
} else if (validation.requiresAcceptance && this.toolUse?.name === 'executeBash') {
4955
this.messenger.sendDirectiveMessage(
5056
tabID,
5157
triggerID,

packages/core/src/codewhispererChat/tools/fsRead.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import fs from '../../shared/fs/fs'
88
import { Writable } from 'stream'
99
import { InvokeOutput, OutputKind, sanitizePath, CommandValidation, fsReadToolResponseSize } from './toolShared'
1010
import { isInDirectory } from '../../shared/filesystemUtilities'
11+
import path from 'path'
1112

1213
export interface FsReadParams {
1314
path: string
@@ -47,8 +48,28 @@ export class FsRead {
4748
this.logger.debug(`Validation succeeded for path: ${this.fsPath}`)
4849
}
4950

50-
public queueDescription(updates: Writable): void {
51-
updates.write('')
51+
public queueDescription(updates: Writable, requiresAcceptance: boolean): void {
52+
if (requiresAcceptance) {
53+
const fileName = path.basename(this.fsPath)
54+
const fileUri = vscode.Uri.file(this.fsPath)
55+
updates.write(`Reading file: [${fileName}](${fileUri}), `)
56+
57+
const [start, end] = this.readRange ?? []
58+
59+
if (start && end) {
60+
updates.write(`from line ${start} to ${end}`)
61+
} else if (start) {
62+
if (start > 0) {
63+
updates.write(`from line ${start} to end of file`)
64+
} else {
65+
updates.write(`${start} line from the end of file to end of file`)
66+
}
67+
} else {
68+
updates.write('all lines')
69+
}
70+
} else {
71+
updates.write('')
72+
}
5273
updates.end()
5374
}
5475

packages/core/src/codewhispererChat/tools/listDirectory.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,21 @@ export class ListDirectory {
4949
}
5050
}
5151

52-
public queueDescription(updates: Writable): void {
53-
const fileName = path.basename(this.fsPath)
54-
if (this.maxDepth === undefined) {
55-
updates.write(`Analyzing directories recursively: ${fileName}`)
56-
} else if (this.maxDepth === 0) {
57-
updates.write(`Analyzing directory: ${fileName}`)
52+
public queueDescription(updates: Writable, requiresAcceptance: boolean): void {
53+
if (requiresAcceptance) {
54+
const fileName = path.basename(this.fsPath)
55+
if (this.maxDepth === undefined) {
56+
updates.write(`Analyzing directories recursively: ${fileName}`)
57+
} else if (this.maxDepth === 0) {
58+
updates.write(`Analyzing directory: ${fileName}`)
59+
} else {
60+
const level = this.maxDepth > 1 ? 'levels' : 'level'
61+
updates.write(`Analyzing directory: ${fileName} limited to ${this.maxDepth} subfolder ${level}`)
62+
}
5863
} else {
59-
const level = this.maxDepth > 1 ? 'levels' : 'level'
60-
updates.write(`Analyzing directory: ${fileName} limited to ${this.maxDepth} subfolder ${level}`)
64+
updates.write('')
6165
}
66+
6267
updates.end()
6368
}
6469

packages/core/src/codewhispererChat/tools/toolUtils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5+
import * as vscode from 'vscode'
56
import { Writable } from 'stream'
67
import { FsRead, FsReadParams } from './fsRead'
78
import { FsWrite, FsWriteParams } from './fsWrite'
@@ -14,7 +15,6 @@ import {
1415
fsReadToolResponseSize,
1516
} from './toolShared'
1617
import { ListDirectory, ListDirectoryParams } from './listDirectory'
17-
import * as vscode from 'vscode'
1818

1919
export enum ToolType {
2020
FsRead = 'fsRead',
@@ -95,10 +95,10 @@ export class ToolUtils {
9595
}
9696
}
9797

98-
static async queueDescription(tool: Tool, updates: Writable): Promise<void> {
98+
static async queueDescription(tool: Tool, updates: Writable, requiresAcceptance: boolean): Promise<void> {
9999
switch (tool.type) {
100100
case ToolType.FsRead:
101-
tool.tool.queueDescription(updates)
101+
tool.tool.queueDescription(updates, requiresAcceptance)
102102
break
103103
case ToolType.FsWrite:
104104
await tool.tool.queueDescription(updates)
@@ -107,7 +107,7 @@ export class ToolUtils {
107107
tool.tool.queueDescription(updates)
108108
break
109109
case ToolType.ListDirectory:
110-
tool.tool.queueDescription(updates)
110+
tool.tool.queueDescription(updates, requiresAcceptance)
111111
break
112112
}
113113
}

packages/core/src/test/codewhispererChat/tools/toolShared.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,30 +234,30 @@ describe('ToolUtils', function () {
234234
// TODO: Adding "void" to the following tests for the current implementation but in the next followup PR I will fix this issue.
235235
it('delegates to FsRead tool queueDescription method', function () {
236236
const tool: Tool = { type: ToolType.FsRead, tool: mockFsRead as unknown as FsRead }
237-
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable)
237+
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable, false)
238238

239-
assert(mockFsRead.queueDescription.calledOnceWith(mockWritable))
239+
assert(mockFsRead.queueDescription.calledOnceWith(mockWritable, false))
240240
})
241241

242242
it('delegates to FsWrite tool queueDescription method', function () {
243243
const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite }
244-
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable)
244+
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable, false)
245245

246246
assert(mockFsWrite.queueDescription.calledOnceWith(mockWritable))
247247
})
248248

249249
it('delegates to ExecuteBash tool queueDescription method', function () {
250250
const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash }
251-
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable)
251+
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable, false)
252252

253253
assert(mockExecuteBash.queueDescription.calledOnceWith(mockWritable))
254254
})
255255

256256
it('delegates to ListDirectory tool queueDescription method', function () {
257257
const tool: Tool = { type: ToolType.ListDirectory, tool: mockListDirectory as unknown as ListDirectory }
258-
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable)
258+
void ToolUtils.queueDescription(tool, mockWritable as unknown as Writable, false)
259259

260-
assert(mockListDirectory.queueDescription.calledOnceWith(mockWritable))
260+
assert(mockListDirectory.queueDescription.calledOnceWith(mockWritable, false))
261261
})
262262
})
263263

0 commit comments

Comments
 (0)