Skip to content

Commit c417219

Browse files
authored
Merge pull request microsoft#256793 from microsoft/tyriar/256280_commandlines__explain
Explain why a command/command line was auto approved or not
2 parents 6b5ea98 + 7176efa commit c417219

File tree

4 files changed

+123
-51
lines changed

4 files changed

+123
-51
lines changed

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import { IConfigurationService } from '../../../../../platform/configuration/com
99
import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js';
1010
import { isPowerShell } from './runInTerminalHelpers.js';
1111

12+
interface IAutoApproveRule {
13+
regex: RegExp;
14+
sourceText: string;
15+
}
16+
1217
export class CommandLineAutoApprover extends Disposable {
13-
private _denyListRegexes: RegExp[] = [];
14-
private _allowListRegexes: RegExp[] = [];
15-
private _allowListCommandLineRegexes: RegExp[] = [];
16-
private _denyListCommandLineRegexes: RegExp[] = [];
18+
private _denyListRules: IAutoApproveRule[] = [];
19+
private _allowListRules: IAutoApproveRule[] = [];
20+
private _allowListCommandLineRules: IAutoApproveRule[] = [];
21+
private _denyListCommandLineRules: IAutoApproveRule[] = [];
1722

1823
constructor(
1924
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -28,49 +33,49 @@ export class CommandLineAutoApprover extends Disposable {
2833
}
2934

3035
updateConfiguration() {
31-
const { denyList, allowList, allowListCommandLine, denyListCommandLine } = this._mapAutoApproveConfigToRegexList(this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoApprove));
32-
this._allowListRegexes = allowList;
33-
this._denyListRegexes = denyList;
34-
this._allowListCommandLineRegexes = allowListCommandLine;
35-
this._denyListCommandLineRegexes = denyListCommandLine;
36+
const { denyListRules, allowListRules, allowListCommandLineRules, denyListCommandLineRules } = this._mapAutoApproveConfigToRules(this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoApprove));
37+
this._allowListRules = allowListRules;
38+
this._denyListRules = denyListRules;
39+
this._allowListCommandLineRules = allowListCommandLineRules;
40+
this._denyListCommandLineRules = denyListCommandLineRules;
3641
}
3742

38-
isCommandAutoApproved(command: string, shell: string, os: OperatingSystem): boolean {
43+
isCommandAutoApproved(command: string, shell: string, os: OperatingSystem): { isAutoApproved: boolean; reason: string } {
3944
// Check the deny list to see if this command requires explicit approval
40-
for (const regex of this._denyListRegexes) {
41-
if (this._commandMatchesRegex(regex, command, shell, os)) {
42-
return false;
45+
for (const rule of this._denyListRules) {
46+
if (this._commandMatchesRegex(rule.regex, command, shell, os)) {
47+
return { isAutoApproved: false, reason: `Command '${command}' is denied by deny list rule: ${rule.sourceText}` };
4348
}
4449
}
4550

4651
// Check the allow list to see if the command is allowed to run without explicit approval
47-
for (const regex of this._allowListRegexes) {
48-
if (this._commandMatchesRegex(regex, command, shell, os)) {
49-
return true;
52+
for (const rule of this._allowListRules) {
53+
if (this._commandMatchesRegex(rule.regex, command, shell, os)) {
54+
return { isAutoApproved: true, reason: `Command '${command}' is approved by allow list rule: ${rule.sourceText}` };
5055
}
5156
}
5257

5358
// TODO: LLM-based auto-approval https://github.com/microsoft/vscode/issues/253267
5459

5560
// Fallback is always to require approval
56-
return false;
61+
return { isAutoApproved: false, reason: `Command '${command}' has no matching auto approve entries` };
5762
}
5863

59-
isCommandLineAutoApproved(commandLine: string): boolean {
64+
isCommandLineAutoApproved(commandLine: string): { isAutoApproved: boolean; reason: string } {
6065
// Check the deny list first to see if this command line requires explicit approval
61-
for (const regex of this._denyListCommandLineRegexes) {
62-
if (regex.test(commandLine)) {
63-
return false;
66+
for (const rule of this._denyListCommandLineRules) {
67+
if (rule.regex.test(commandLine)) {
68+
return { isAutoApproved: false, reason: `Command line '${commandLine}' is denied by deny list rule: ${rule.sourceText}` };
6469
}
6570
}
6671

6772
// Check if the full command line matches any of the allow list command line regexes
68-
for (const regex of this._allowListCommandLineRegexes) {
69-
if (regex.test(commandLine)) {
70-
return true;
73+
for (const rule of this._allowListCommandLineRules) {
74+
if (rule.regex.test(commandLine)) {
75+
return { isAutoApproved: true, reason: `Command line '${commandLine}' is approved by allow list rule: ${rule.sourceText}` };
7176
}
7277
}
73-
return false;
78+
return { isAutoApproved: false, reason: `Command line '${commandLine}' has no matching auto approve entries` };
7479
}
7580

7681
private _commandMatchesRegex(regex: RegExp, command: string, shell: string, os: OperatingSystem): boolean {
@@ -86,24 +91,34 @@ export class CommandLineAutoApprover extends Disposable {
8691
return false;
8792
}
8893

89-
private _mapAutoApproveConfigToRegexList(config: unknown): { denyList: RegExp[]; allowList: RegExp[]; allowListCommandLine: RegExp[]; denyListCommandLine: RegExp[] } {
94+
private _mapAutoApproveConfigToRules(config: unknown): {
95+
denyListRules: IAutoApproveRule[];
96+
allowListRules: IAutoApproveRule[];
97+
allowListCommandLineRules: IAutoApproveRule[];
98+
denyListCommandLineRules: IAutoApproveRule[];
99+
} {
90100
if (!config || typeof config !== 'object') {
91-
return { denyList: [], allowList: [], allowListCommandLine: [], denyListCommandLine: [] };
101+
return {
102+
denyListRules: [],
103+
allowListRules: [],
104+
allowListCommandLineRules: [],
105+
denyListCommandLineRules: []
106+
};
92107
}
93108

94-
const denyList: RegExp[] = [];
95-
const allowList: RegExp[] = [];
96-
const allowListCommandLine: RegExp[] = [];
97-
const denyListCommandLine: RegExp[] = [];
109+
const denyListRules: IAutoApproveRule[] = [];
110+
const allowListRules: IAutoApproveRule[] = [];
111+
const allowListCommandLineRules: IAutoApproveRule[] = [];
112+
const denyListCommandLineRules: IAutoApproveRule[] = [];
98113

99114
Object.entries(config).forEach(([key, value]) => {
100115
if (typeof value === 'boolean') {
101116
const regex = this._convertAutoApproveEntryToRegex(key);
102117
// IMPORTANT: Only true and false are used, null entries need to be ignored
103118
if (value === true) {
104-
allowList.push(regex);
119+
allowListRules.push({ regex, sourceText: key });
105120
} else if (value === false) {
106-
denyList.push(regex);
121+
denyListRules.push({ regex, sourceText: key });
107122
}
108123
} else if (typeof value === 'object' && value !== null) {
109124
// Handle object format like { approve: true/false, matchCommandLine: true/false }
@@ -112,22 +127,27 @@ export class CommandLineAutoApprover extends Disposable {
112127
const regex = this._convertAutoApproveEntryToRegex(key);
113128
if (objectValue.approve === true) {
114129
if (objectValue.matchCommandLine === true) {
115-
allowListCommandLine.push(regex);
130+
allowListCommandLineRules.push({ regex, sourceText: key });
116131
} else {
117-
allowList.push(regex);
132+
allowListRules.push({ regex, sourceText: key });
118133
}
119134
} else if (objectValue.approve === false) {
120135
if (objectValue.matchCommandLine === true) {
121-
denyListCommandLine.push(regex);
136+
denyListCommandLineRules.push({ regex, sourceText: key });
122137
} else {
123-
denyList.push(regex);
138+
denyListRules.push({ regex, sourceText: key });
124139
}
125140
}
126141
}
127142
}
128143
});
129144

130-
return { denyList, allowList, allowListCommandLine, denyListCommandLine };
145+
return {
146+
denyListRules,
147+
allowListRules,
148+
allowListCommandLineRules,
149+
denyListCommandLineRules
150+
};
131151
}
132152

133153
private _convertAutoApproveEntryToRegex(value: string): RegExp {

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,34 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
171171
const subCommands = splitCommandLineIntoSubCommands(args.command, shell, os);
172172
const inlineSubCommands = subCommands.map(e => Array.from(extractInlineSubCommands(e, shell, os))).flat();
173173
const allSubCommands = [...subCommands, ...inlineSubCommands];
174-
if (allSubCommands.every(e => this._commandLineAutoApprover.isCommandAutoApproved(e, shell, os))) {
174+
const subCommandResults = allSubCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, shell, os));
175+
const autoApproveReasons: string[] = [...subCommandResults.map(e => e.reason)];
176+
177+
if (subCommandResults.every(e => e.isAutoApproved)) {
178+
this._logService.info('autoApprove: All sub-commands auto-approved');
175179
confirmationMessages = undefined;
176180
} else {
177-
if (this._commandLineAutoApprover.isCommandLineAutoApproved(args.command)) {
181+
this._logService.info('autoApprove: All sub-commands NOT auto-approved');
182+
const commandLineResults = this._commandLineAutoApprover.isCommandLineAutoApproved(args.command);
183+
autoApproveReasons.push(commandLineResults.reason);
184+
if (commandLineResults.isAutoApproved) {
185+
this._logService.info('autoApprove: Command line auto-approved');
178186
confirmationMessages = undefined;
179187
} else {
188+
this._logService.info('autoApprove: Command line NOT auto-approved');
180189
confirmationMessages = {
181190
title: args.isBackground
182191
? localize('runInTerminal.background', "Run command in background terminal")
183192
: localize('runInTerminal.foreground', "Run command in terminal"),
184-
message: new MarkdownString(
185-
args.explanation
186-
),
193+
message: new MarkdownString(args.explanation),
187194
};
188195
}
189196
}
197+
198+
// TODO: Surface reason on tool part https://github.com/microsoft/vscode/pull/256793
199+
for (const reason of autoApproveReasons) {
200+
this._logService.info(`- ${reason}`);
201+
}
190202
}
191203

192204
const instance = context.chatSessionId ? this._sessionTerminalAssociations.get(context.chatSessionId)?.instance : undefined;

src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
3535
localize('autoApprove.description.intro', "A list of commands or regular expressions that control whether the run in terminal tool commands require explicit approval. These will be matched against the start of a command. A regular expression can be provided by wrapping the string in {0} characters followed by optional flags such as {1} for case-insensitivity.", '`/`', '`i`'),
3636
localize('autoApprove.description.values', "Set to {0} to automatically approve commands, {1} to always require explicit approval or {2} to unset the value.", '`true`', '`false`', '`null`'),
3737
localize('autoApprove.description.subCommands', "Note that these commands and regular expressions are evaluated for every _sub-command_ within the full _command line_, so {0} for example will need both {1} and {2} to match a {3} entry and must not match a {4} entry in order to auto approve. Inline commands are also detected so {5} will need both {5} and {6} to pass.", '`foo && bar`', '`foo`', '`bar`', '`true`', '`false`', '`echo $(rm file)`', '`rm file`'),
38-
localize('autoApprove.description.commandLine', "An object can be used to match against the full command line instead of matching sub-commands and inline commands, for example {0}. This will be checked _after_ sub-commands are checked, so denied sub-commands will take precedence.", '`{ approve: false, matchCommandLine: true }`'),
38+
localize('autoApprove.description.commandLine', "An object can be used to match against the full command line instead of matching sub-commands and inline commands, for example {0}. This will be checked _after_ sub-commands are checked, taking precedence over even denied sub-commands.", '`{ approve: false, matchCommandLine: true }`'),
3939
[
4040
localize('autoApprove.description.examples.title', 'Examples:'),
4141
`|${localize('autoApprove.description.examples.value', "Value")}|${localize('autoApprove.description.examples.description', "Description")}|`,

src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/commandLineAutoApprover.test.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { workbenchInstantiationService } from '../../../../../test/browser/workb
1111
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';
1212
import { CommandLineAutoApprover } from '../../browser/commandLineAutoApprover.js';
1313
import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js';
14-
import { ok } from 'assert';
14+
import { ok, strictEqual } from 'assert';
1515

1616
suite('CommandLineAutoApprover', () => {
1717
const store = ensureNoDisposablesAreLeakedInTestSuite();
@@ -38,6 +38,10 @@ suite('CommandLineAutoApprover', () => {
3838
setConfig(TerminalChatAgentToolsSettingId.AutoApprove, value);
3939
}
4040

41+
function setAutoApproveWithCommandLine(value: { [key: string]: { approve: boolean; matchCommandLine?: boolean } | boolean }) {
42+
setConfig(TerminalChatAgentToolsSettingId.AutoApprove, value);
43+
}
44+
4145
function setConfig(key: string, value: unknown) {
4246
configurationService.setUserConfiguration(key, value);
4347
configurationService.onDidChangeConfigurationEmitter.fire({
@@ -49,11 +53,11 @@ suite('CommandLineAutoApprover', () => {
4953
}
5054

5155
function isAutoApproved(commandLine: string): boolean {
52-
return commandLineAutoApprover.isCommandAutoApproved(commandLine, shell, os);
56+
return commandLineAutoApprover.isCommandAutoApproved(commandLine, shell, os).isAutoApproved;
5357
}
5458

5559
function isCommandLineAutoApproved(commandLine: string): boolean {
56-
return commandLineAutoApprover.isCommandLineAutoApproved(commandLine);
60+
return commandLineAutoApprover.isCommandLineAutoApproved(commandLine).isAutoApproved;
5761
}
5862

5963
suite('autoApprove with allow patterns only', () => {
@@ -331,10 +335,6 @@ suite('CommandLineAutoApprover', () => {
331335
});
332336

333337
suite('isCommandLineAutoApproved - matchCommandLine functionality', () => {
334-
function setAutoApproveWithCommandLine(value: { [key: string]: { approve: boolean; matchCommandLine: boolean } | boolean }) {
335-
setConfig(TerminalChatAgentToolsSettingId.AutoApprove, value);
336-
}
337-
338338
test('should auto-approve command line patterns with matchCommandLine: true', () => {
339339
setAutoApproveWithCommandLine({
340340
"echo": { approve: true, matchCommandLine: true }
@@ -440,4 +440,44 @@ suite('CommandLineAutoApprover', () => {
440440
ok(!isCommandLineAutoApproved('powershell -File script.ps1 -ExecutionPolicy Bypass'));
441441
});
442442
});
443+
444+
suite('reasons', () => {
445+
function getCommandReason(command: string): string {
446+
return commandLineAutoApprover.isCommandAutoApproved(command, shell, os).reason;
447+
}
448+
449+
function getCommandLineReason(commandLine: string): string {
450+
return commandLineAutoApprover.isCommandLineAutoApproved(commandLine).reason;
451+
}
452+
453+
suite('command', () => {
454+
test('approved', () => {
455+
setAutoApprove({ echo: true });
456+
strictEqual(getCommandReason('echo hello'), `Command 'echo hello' is approved by allow list rule: echo`);
457+
});
458+
test('not approved', () => {
459+
setAutoApprove({ echo: false });
460+
strictEqual(getCommandReason('echo hello'), `Command 'echo hello' is denied by deny list rule: echo`);
461+
});
462+
test('no match', () => {
463+
setAutoApprove({});
464+
strictEqual(getCommandReason('echo hello'), `Command 'echo hello' has no matching auto approve entries`);
465+
});
466+
});
467+
468+
suite('command line', () => {
469+
test('approved', () => {
470+
setAutoApproveWithCommandLine({ echo: { approve: true, matchCommandLine: true } });
471+
strictEqual(getCommandLineReason('echo hello'), `Command line 'echo hello' is approved by allow list rule: echo`);
472+
});
473+
test('not approved', () => {
474+
setAutoApproveWithCommandLine({ echo: { approve: false, matchCommandLine: true } });
475+
strictEqual(getCommandLineReason('echo hello'), `Command line 'echo hello' is denied by deny list rule: echo`);
476+
});
477+
test('no match', () => {
478+
setAutoApproveWithCommandLine({});
479+
strictEqual(getCommandLineReason('echo hello'), `Command line 'echo hello' has no matching auto approve entries`);
480+
});
481+
});
482+
});
443483
});

0 commit comments

Comments
 (0)