Skip to content

Commit 0b97f91

Browse files
authored
Merge pull request #285925 from microsoft/tyriar/285921
Show message about denied commands with links
2 parents 8ebff69 + 298ef56 commit 0b97f91

File tree

4 files changed

+127
-9
lines changed

4 files changed

+127
-9
lines changed

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export interface ICommandLineAnalyzerResult {
6060
* - `undefined`: This analyzer does not make an approval/denial decision
6161
*/
6262
readonly isAutoApproved?: boolean;
63-
readonly disclaimers?: readonly string[];
63+
readonly disclaimers?: readonly (string | IMarkdownString)[];
6464
readonly autoApproveInfo?: IMarkdownString;
6565
readonly customActions?: ToolConfirmationAction[];
6666
}

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
161161
});
162162

163163
// Prompt injection warning for common commands that return content from the web
164-
const disclaimers: string[] = [];
164+
const disclaimers: (string | IMarkdownString)[] = [];
165165
const subCommandsLowerFirstWordOnly = subCommands.map(command => command.split(' ')[0].toLowerCase());
166166
if (!isAutoApproved && (
167167
subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLower.includes(command)) ||
@@ -170,6 +170,20 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
170170
disclaimers.push(localize('runInTerminal.promptInjectionDisclaimer', 'Web content may contain malicious code or attempt prompt injection attacks.'));
171171
}
172172

173+
// Add denial reason to disclaimers when auto-approve is enabled but command was denied by a rule
174+
if (isAutoApproveEnabled && isDenied) {
175+
const denialInfo = this._createAutoApproveInfo(
176+
isAutoApproved,
177+
isDenied,
178+
autoApproveReason,
179+
subCommandResults,
180+
commandLineResult,
181+
);
182+
if (denialInfo) {
183+
disclaimers.push(denialInfo);
184+
}
185+
}
186+
173187
if (!isAutoApproved && isAutoApproveEnabled) {
174188
customActions = generateAutoApproveActions(trimmedCommandLine, subCommands, { subCommandResults, commandLineResult });
175189
}
@@ -270,9 +284,9 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
270284
case 'subCommand': {
271285
const uniqueRules = dedupeRules(subCommandResults.filter(e => e.result === 'denied'));
272286
if (uniqueRules.length === 1) {
273-
return new MarkdownString(localize('autoApproveDenied.rule', 'Auto approval denied by rule {0}', formatRuleLinks(uniqueRules)));
287+
return new MarkdownString(localize('autoApproveDenied.rule', 'Auto approval denied by rule {0}', formatRuleLinks(uniqueRules)), mdTrustSettings);
274288
} else if (uniqueRules.length > 1) {
275-
return new MarkdownString(localize('autoApproveDenied.rules', 'Auto approval denied by rules {0}', formatRuleLinks(uniqueRules)));
289+
return new MarkdownString(localize('autoApproveDenied.rules', 'Auto approval denied by rules {0}', formatRuleLinks(uniqueRules)), mdTrustSettings);
276290
}
277291
break;
278292
}

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { TerminalCommandArtifactCollector } from './terminalCommandArtifactColle
5555
import { isNumber, isString } from '../../../../../../base/common/types.js';
5656
import { ChatConfiguration } from '../../../../chat/common/constants.js';
5757
import { IChatWidgetService } from '../../../../chat/browser/chat.js';
58+
import { TerminalChatCommandId } from '../../../chat/browser/terminalChat.js';
5859

5960
// #region Tool data
6061

@@ -445,10 +446,15 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
445446
};
446447
const commandLineAnalyzerResults = await Promise.all(this._commandLineAnalyzers.map(e => e.analyze(commandLineAnalyzerOptions)));
447448

448-
const disclaimersRaw = commandLineAnalyzerResults.filter(e => e.disclaimers).flatMap(e => e.disclaimers);
449+
const disclaimersRaw = commandLineAnalyzerResults.map(e => e.disclaimers).filter(e => !!e).flatMap(e => e);
449450
let disclaimer: IMarkdownString | undefined;
450451
if (disclaimersRaw.length > 0) {
451-
disclaimer = new MarkdownString(`$(${Codicon.info.id}) ` + disclaimersRaw.join(' '), { supportThemeIcons: true });
452+
const disclaimerTexts = disclaimersRaw.map(d => typeof d === 'string' ? d : d.value);
453+
const hasMarkdownDisclaimer = disclaimersRaw.some(d => typeof d !== 'string');
454+
const mdOptions = hasMarkdownDisclaimer
455+
? { supportThemeIcons: true, isTrusted: { enabledCommands: [TerminalChatCommandId.OpenTerminalSettingsLink] } }
456+
: { supportThemeIcons: true };
457+
disclaimer = new MarkdownString(`$(${Codicon.info.id}) ` + disclaimerTexts.join(' '), mdOptions);
452458
}
453459

454460
const analyzersIsAutoApproveAllowed = commandLineAnalyzerResults.every(e => e.isAutoApproveAllowed);
@@ -476,9 +482,13 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
476482
wouldBeAutoApproved
477483
);
478484

479-
// Pass autoApproveInfo if command would be auto-approved (even if warning not yet accepted)
480-
// This allows the confirmation widget to auto-approve after user accepts the warning
481-
if (isFinalAutoApproved || (isAutoApproveEnabled && wouldBeAutoApproved)) {
485+
// Pass auto approve info if the command:
486+
// - Was auto approved
487+
// - Would have be auto approved, but the opt-in warning was not accepted
488+
// - Was denied explicitly by a rule
489+
//
490+
// This allows surfacing this information to the user.
491+
if (isFinalAutoApproved || (isAutoApproveEnabled && commandLineAnalyzerResults.some(e => e.autoApproveInfo))) {
482492
toolSpecificData.autoApproveInfo = commandLineAnalyzerResults.find(e => e.autoApproveInfo)?.autoApproveInfo;
483493
}
484494

src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { RunInTerminalTool, type IRunInTerminalInputParams } from '../../browser
3939
import { ShellIntegrationQuality } from '../../browser/toolTerminalCreator.js';
4040
import { terminalChatAgentToolsConfiguration, TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';
4141
import { TerminalChatService } from '../../../chat/browser/terminalChatService.js';
42+
import type { IMarkdownString } from '../../../../../../base/common/htmlContent.js';
4243

4344
class TestRunInTerminalTool extends RunInTerminalTool {
4445
protected override _osBackend: Promise<OperatingSystem> = Promise.resolve(OperatingSystem.Windows);
@@ -1213,4 +1214,97 @@ suite('RunInTerminalTool', () => {
12131214
});
12141215
});
12151216
});
1217+
1218+
suite('denial info in disclaimers', () => {
1219+
function getDisclaimerValue(disclaimer: string | IMarkdownString | undefined): string | undefined {
1220+
if (!disclaimer) {
1221+
return undefined;
1222+
}
1223+
return typeof disclaimer === 'string' ? disclaimer : disclaimer.value;
1224+
}
1225+
1226+
test('should include denial reason in disclaimer when command is denied by rule', async () => {
1227+
setAutoApprove({
1228+
npm: { approve: false }
1229+
});
1230+
const result = await executeToolTest({
1231+
command: 'npm run build',
1232+
explanation: 'Build the project'
1233+
});
1234+
1235+
assertConfirmationRequired(result, 'Run `bash` command?');
1236+
const disclaimerValue = getDisclaimerValue(result?.confirmationMessages?.disclaimer);
1237+
ok(disclaimerValue, 'Expected disclaimer to be defined');
1238+
ok(disclaimerValue.includes('denied'), 'Expected disclaimer to mention denial');
1239+
ok(disclaimerValue.includes('npm'), 'Expected disclaimer to mention the denied rule');
1240+
});
1241+
1242+
test('should include link to settings in denial disclaimer', async () => {
1243+
setAutoApprove({
1244+
rm: { approve: false }
1245+
});
1246+
const result = await executeToolTest({
1247+
command: 'rm -rf temp',
1248+
explanation: 'Remove temp folder'
1249+
});
1250+
1251+
assertConfirmationRequired(result, 'Run `bash` command?');
1252+
ok(result?.confirmationMessages?.disclaimer, 'Expected disclaimer to be defined');
1253+
// The disclaimer should have trusted commands enabled for settings links
1254+
const disclaimer = result.confirmationMessages.disclaimer;
1255+
ok(typeof disclaimer !== 'string' && disclaimer.isTrusted, 'Expected disclaimer to be trusted for command links');
1256+
});
1257+
1258+
test('should include denial reason for multiple denied sub-commands', async () => {
1259+
setAutoApprove({
1260+
rm: { approve: false },
1261+
sudo: { approve: false }
1262+
});
1263+
const result = await executeToolTest({
1264+
command: 'sudo rm -rf /',
1265+
explanation: 'Dangerous command'
1266+
});
1267+
1268+
assertConfirmationRequired(result, 'Run `bash` command?');
1269+
const disclaimerValue = getDisclaimerValue(result?.confirmationMessages?.disclaimer);
1270+
ok(disclaimerValue, 'Expected disclaimer to be defined');
1271+
ok(disclaimerValue.includes('denied'), 'Expected disclaimer to mention denial');
1272+
});
1273+
1274+
test('should not include denial info when auto-approve is disabled', async () => {
1275+
setConfig(TerminalChatAgentToolsSettingId.EnableAutoApprove, false);
1276+
setAutoApprove({
1277+
npm: { approve: false }
1278+
});
1279+
const result = await executeToolTest({
1280+
command: 'npm run build',
1281+
explanation: 'Build the project'
1282+
});
1283+
1284+
assertConfirmationRequired(result, 'Run `bash` command?');
1285+
// When auto-approve is disabled, there should be no denial-related disclaimer
1286+
const disclaimerValue = getDisclaimerValue(result?.confirmationMessages?.disclaimer);
1287+
if (disclaimerValue) {
1288+
ok(!disclaimerValue.includes('denied'), 'Should not mention denial when auto-approve is disabled');
1289+
}
1290+
});
1291+
1292+
test('should not include denial info for commands that are simply not approved', async () => {
1293+
// Command is not in auto-approve list, but not explicitly denied
1294+
setAutoApprove({
1295+
echo: true
1296+
});
1297+
const result = await executeToolTest({
1298+
command: 'npm run build',
1299+
explanation: 'Build the project'
1300+
});
1301+
1302+
assertConfirmationRequired(result, 'Run `bash` command?');
1303+
// There should be no denial disclaimer since npm is not explicitly denied
1304+
const disclaimerValue = getDisclaimerValue(result?.confirmationMessages?.disclaimer);
1305+
if (disclaimerValue) {
1306+
ok(!disclaimerValue.includes('denied'), 'Should not mention denial for non-denied commands');
1307+
}
1308+
});
1309+
});
12161310
});

0 commit comments

Comments
 (0)