mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Merge pull request #274088 from microsoft/tyriar/analyzer
Pull auto approve into new analyzer
This commit is contained in:
@@ -3,11 +3,14 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { IMarkdownString } from '../../../../../../../base/common/htmlContent.js';
|
||||
import type { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import type { OperatingSystem } from '../../../../../../../base/common/platform.js';
|
||||
import type { ToolConfirmationAction } from '../../../../../chat/common/languageModelToolsService.js';
|
||||
import type { ITerminalInstance } from '../../../../../terminal/browser/terminal.js';
|
||||
import type { TreeSitterCommandParserLanguage } from '../../treeSitterCommandParser.js';
|
||||
|
||||
export interface ICommandLineAnalyzer {
|
||||
export interface ICommandLineAnalyzer extends IDisposable {
|
||||
analyze(options: ICommandLineAnalyzerOptions): Promise<ICommandLineAnalyzerResult>;
|
||||
}
|
||||
|
||||
@@ -17,9 +20,12 @@ export interface ICommandLineAnalyzerOptions {
|
||||
shell: string;
|
||||
os: OperatingSystem;
|
||||
treeSitterLanguage: TreeSitterCommandParserLanguage;
|
||||
terminalToolSessionId: string;
|
||||
}
|
||||
|
||||
export interface ICommandLineAnalyzerResult {
|
||||
readonly isAutoApproveAllowed: boolean;
|
||||
readonly disclaimers: string[];
|
||||
readonly disclaimers?: readonly string[];
|
||||
readonly autoApproveInfo?: IMarkdownString;
|
||||
readonly customActions?: ToolConfirmationAction[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { asArray } from '../../../../../../../base/common/arrays.js';
|
||||
import { createCommandUri, MarkdownString, type IMarkdownString } from '../../../../../../../base/common/htmlContent.js';
|
||||
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import type { SingleOrMany } from '../../../../../../../base/common/types.js';
|
||||
import { localize } from '../../../../../../../nls.js';
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope } from '../../../../../../../platform/storage/common/storage.js';
|
||||
import { TerminalToolConfirmationStorageKeys } from '../../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js';
|
||||
import { openTerminalSettingsLinkCommandId } from '../../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.js';
|
||||
import { ChatConfiguration } from '../../../../../chat/common/constants.js';
|
||||
import type { ToolConfirmationAction } from '../../../../../chat/common/languageModelToolsService.js';
|
||||
import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAgentToolsConfiguration.js';
|
||||
import { CommandLineAutoApprover, type IAutoApproveRule, type ICommandApprovalResult, type ICommandApprovalResultWithReason } from '../../commandLineAutoApprover.js';
|
||||
import { dedupeRules, generateAutoApproveActions, isPowerShell } from '../../runInTerminalHelpers.js';
|
||||
import type { RunInTerminalToolTelemetry } from '../../runInTerminalToolTelemetry.js';
|
||||
import { type TreeSitterCommandParser } from '../../treeSitterCommandParser.js';
|
||||
import type { ICommandLineAnalyzer, ICommandLineAnalyzerOptions, ICommandLineAnalyzerResult } from './commandLineAnalyzer.js';
|
||||
|
||||
const promptInjectionWarningCommandsLower = [
|
||||
'curl',
|
||||
'wget',
|
||||
];
|
||||
const promptInjectionWarningCommandsLowerPwshOnly = [
|
||||
'invoke-restmethod',
|
||||
'invoke-webrequest',
|
||||
'irm',
|
||||
'iwr',
|
||||
];
|
||||
|
||||
export class CommandLineAutoApproveAnalyzer extends Disposable implements ICommandLineAnalyzer {
|
||||
private readonly _commandLineAutoApprover: CommandLineAutoApprover;
|
||||
|
||||
constructor(
|
||||
private readonly _treeSitterCommandParser: TreeSitterCommandParser,
|
||||
private readonly _telemetry: RunInTerminalToolTelemetry,
|
||||
private readonly _log: (message: string, ...args: unknown[]) => void,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this._commandLineAutoApprover = this._register(instantiationService.createInstance(CommandLineAutoApprover));
|
||||
}
|
||||
|
||||
async analyze(options: ICommandLineAnalyzerOptions): Promise<ICommandLineAnalyzerResult> {
|
||||
let subCommands: string[] | undefined;
|
||||
try {
|
||||
subCommands = await this._treeSitterCommandParser.extractSubCommands(options.treeSitterLanguage, options.commandLine);
|
||||
this._log(`Parsed sub-commands via ${options.treeSitterLanguage} grammar`, subCommands);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this._log(`Failed to parse sub-commands via ${options.treeSitterLanguage} grammar`);
|
||||
}
|
||||
|
||||
let isAutoApproved = false;
|
||||
let autoApproveInfo: IMarkdownString | undefined;
|
||||
let customActions: ToolConfirmationAction[] | undefined;
|
||||
|
||||
if (!subCommands) {
|
||||
return {
|
||||
isAutoApproveAllowed: false,
|
||||
disclaimers: [],
|
||||
};
|
||||
}
|
||||
|
||||
const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os));
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine);
|
||||
const autoApproveReasons: string[] = [
|
||||
...subCommandResults.map(e => e.reason),
|
||||
commandLineResult.reason,
|
||||
];
|
||||
|
||||
let isDenied = false;
|
||||
let autoApproveReason: 'subCommand' | 'commandLine' | undefined;
|
||||
let autoApproveDefault: boolean | undefined;
|
||||
|
||||
const deniedSubCommandResult = subCommandResults.find(e => e.result === 'denied');
|
||||
if (deniedSubCommandResult) {
|
||||
this._log('Sub-command DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = deniedSubCommandResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'subCommand';
|
||||
} else if (commandLineResult.result === 'denied') {
|
||||
this._log('Command line DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'commandLine';
|
||||
} else {
|
||||
if (subCommandResults.every(e => e.result === 'approved')) {
|
||||
this._log('All sub-commands auto-approved');
|
||||
autoApproveReason = 'subCommand';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = subCommandResults.every(e => e.rule?.isDefaultRule);
|
||||
} else {
|
||||
this._log('All sub-commands NOT auto-approved');
|
||||
if (commandLineResult.result === 'approved') {
|
||||
this._log('Command line auto-approved');
|
||||
autoApproveReason = 'commandLine';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
} else {
|
||||
this._log('Command line NOT auto-approved');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log detailed auto approval reasoning
|
||||
for (const reason of autoApproveReasons) {
|
||||
this._log(`- ${reason}`);
|
||||
}
|
||||
|
||||
// Apply auto approval or force it off depending on enablement/opt-in state
|
||||
const isAutoApproveEnabled = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnableAutoApprove) === true;
|
||||
const isAutoApproveWarningAccepted = this._storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false);
|
||||
if (isAutoApproveEnabled && isAutoApproved) {
|
||||
autoApproveInfo = this._createAutoApproveInfo(
|
||||
isAutoApproved,
|
||||
isDenied,
|
||||
autoApproveReason,
|
||||
subCommandResults,
|
||||
commandLineResult,
|
||||
);
|
||||
} else {
|
||||
isAutoApproved = false;
|
||||
}
|
||||
|
||||
// Send telemetry about auto approval process
|
||||
this._telemetry.logPrepare({
|
||||
terminalToolSessionId: options.terminalToolSessionId,
|
||||
subCommands,
|
||||
autoApproveAllowed: !isAutoApproveEnabled ? 'off' : isAutoApproveWarningAccepted ? 'allowed' : 'needsOptIn',
|
||||
autoApproveResult: isAutoApproved ? 'approved' : isDenied ? 'denied' : 'manual',
|
||||
autoApproveReason,
|
||||
autoApproveDefault
|
||||
});
|
||||
|
||||
// Prompt injection warning for common commands that return content from the web
|
||||
const disclaimers: string[] = [];
|
||||
const subCommandsLowerFirstWordOnly = subCommands.map(command => command.split(' ')[0].toLowerCase());
|
||||
if (!isAutoApproved && (
|
||||
subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLower.includes(command)) ||
|
||||
(isPowerShell(options.shell, options.os) && subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLowerPwshOnly.includes(command)))
|
||||
)) {
|
||||
disclaimers.push(localize('runInTerminal.promptInjectionDisclaimer', 'Web content may contain malicious code or attempt prompt injection attacks.'));
|
||||
}
|
||||
|
||||
if (!isAutoApproved && isAutoApproveEnabled) {
|
||||
customActions = generateAutoApproveActions(options.commandLine, subCommands, { subCommandResults, commandLineResult });
|
||||
}
|
||||
|
||||
return {
|
||||
isAutoApproveAllowed: isAutoApproved,
|
||||
disclaimers,
|
||||
autoApproveInfo,
|
||||
customActions,
|
||||
};
|
||||
}
|
||||
|
||||
private _createAutoApproveInfo(
|
||||
isAutoApproved: boolean,
|
||||
isDenied: boolean,
|
||||
autoApproveReason: 'subCommand' | 'commandLine' | undefined,
|
||||
subCommandResults: ICommandApprovalResultWithReason[],
|
||||
commandLineResult: ICommandApprovalResultWithReason,
|
||||
): IMarkdownString | undefined {
|
||||
const formatRuleLinks = (result: SingleOrMany<{ result: ICommandApprovalResult; rule?: IAutoApproveRule; reason: string }>): string => {
|
||||
return asArray(result).map(e => {
|
||||
const settingsUri = createCommandUri(openTerminalSettingsLinkCommandId, e.rule!.sourceTarget);
|
||||
return `[\`${e.rule!.sourceText}\`](${settingsUri.toString()} "${localize('ruleTooltip', 'View rule in settings')}")`;
|
||||
}).join(', ');
|
||||
};
|
||||
|
||||
const mdTrustSettings = {
|
||||
isTrusted: {
|
||||
enabledCommands: [openTerminalSettingsLinkCommandId]
|
||||
}
|
||||
};
|
||||
|
||||
const config = this._configurationService.inspect<boolean | Record<string, boolean>>(ChatConfiguration.GlobalAutoApprove);
|
||||
const isGlobalAutoApproved = config?.value ?? config.defaultValue;
|
||||
if (isGlobalAutoApproved) {
|
||||
const settingsUri = createCommandUri(openTerminalSettingsLinkCommandId, 'global');
|
||||
return new MarkdownString(`${localize('autoApprove.global', 'Auto approved by setting {0}', `[\`${ChatConfiguration.GlobalAutoApprove}\`](${settingsUri.toString()} "${localize('ruleTooltip.global', 'View settings')}")`)}`, mdTrustSettings);
|
||||
}
|
||||
|
||||
if (isAutoApproved) {
|
||||
switch (autoApproveReason) {
|
||||
case 'commandLine': {
|
||||
if (commandLineResult.rule) {
|
||||
return new MarkdownString(localize('autoApprove.rule', 'Auto approved by rule {0}', formatRuleLinks(commandLineResult)), mdTrustSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'subCommand': {
|
||||
const uniqueRules = dedupeRules(subCommandResults);
|
||||
if (uniqueRules.length === 1) {
|
||||
return new MarkdownString(localize('autoApprove.rule', 'Auto approved by rule {0}', formatRuleLinks(uniqueRules)), mdTrustSettings);
|
||||
} else if (uniqueRules.length > 1) {
|
||||
return new MarkdownString(localize('autoApprove.rules', 'Auto approved by rules {0}', formatRuleLinks(uniqueRules)), mdTrustSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (isDenied) {
|
||||
switch (autoApproveReason) {
|
||||
case 'commandLine': {
|
||||
if (commandLineResult.rule) {
|
||||
return new MarkdownString(localize('autoApproveDenied.rule', 'Auto approval denied by rule {0}', formatRuleLinks(commandLineResult)), mdTrustSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'subCommand': {
|
||||
const uniqueRules = dedupeRules(subCommandResults.filter(e => e.result === 'denied'));
|
||||
if (uniqueRules.length === 1) {
|
||||
return new MarkdownString(localize('autoApproveDenied.rule', 'Auto approval denied by rule {0}', formatRuleLinks(uniqueRules)));
|
||||
} else if (uniqueRules.length > 1) {
|
||||
return new MarkdownString(localize('autoApproveDenied.rules', 'Auto approval denied by rules {0}', formatRuleLinks(uniqueRules)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { localize } from '../../../../../../../nls.js';
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
|
||||
@@ -12,7 +13,7 @@ import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAge
|
||||
import type { TreeSitterCommandParser } from '../../treeSitterCommandParser.js';
|
||||
import type { ICommandLineAnalyzer, ICommandLineAnalyzerOptions, ICommandLineAnalyzerResult } from './commandLineAnalyzer.js';
|
||||
|
||||
export class CommandLineFileWriteAnalyzer implements ICommandLineAnalyzer {
|
||||
export class CommandLineFileWriteAnalyzer extends Disposable implements ICommandLineAnalyzer {
|
||||
constructor(
|
||||
private readonly _treeSitterCommandParser: TreeSitterCommandParser,
|
||||
private readonly _log: (message: string, ...args: unknown[]) => void,
|
||||
@@ -20,6 +21,7 @@ export class CommandLineFileWriteAnalyzer implements ICommandLineAnalyzer {
|
||||
@IHistoryService private readonly _historyService: IHistoryService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async analyze(options: ICommandLineAnalyzerOptions): Promise<ICommandLineAnalyzerResult> {
|
||||
|
||||
@@ -4,18 +4,17 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { IMarker as IXtermMarker } from '@xterm/xterm';
|
||||
import { asArray } from '../../../../../../base/common/arrays.js';
|
||||
import { timeout } from '../../../../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../../../../base/common/codicons.js';
|
||||
import { CancellationError } from '../../../../../../base/common/errors.js';
|
||||
import { Event } from '../../../../../../base/common/event.js';
|
||||
import { createCommandUri, MarkdownString, type IMarkdownString } from '../../../../../../base/common/htmlContent.js';
|
||||
import { MarkdownString, type IMarkdownString } from '../../../../../../base/common/htmlContent.js';
|
||||
import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js';
|
||||
import { basename } from '../../../../../../base/common/path.js';
|
||||
import { OperatingSystem, OS } from '../../../../../../base/common/platform.js';
|
||||
import { count } from '../../../../../../base/common/strings.js';
|
||||
import type { DeepImmutable, SingleOrMany } from '../../../../../../base/common/types.js';
|
||||
import type { DeepImmutable } from '../../../../../../base/common/types.js';
|
||||
import { generateUuid } from '../../../../../../base/common/uuid.js';
|
||||
import { localize } from '../../../../../../nls.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
@@ -25,30 +24,28 @@ import { TerminalCapability } from '../../../../../../platform/terminal/common/c
|
||||
import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js';
|
||||
import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js';
|
||||
import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js';
|
||||
import { openTerminalSettingsLinkCommandId } from '../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.js';
|
||||
import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService.js';
|
||||
import { ChatConfiguration } from '../../../../chat/common/constants.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress, type IToolConfirmationMessages, type ToolConfirmationAction } from '../../../../chat/common/languageModelToolsService.js';
|
||||
import { ITerminalService, type ITerminalInstance, ITerminalChatService } from '../../../../terminal/browser/terminal.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/languageModelToolsService.js';
|
||||
import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js';
|
||||
import type { XtermTerminal } from '../../../../terminal/browser/xterm/xtermTerminal.js';
|
||||
import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js';
|
||||
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';
|
||||
import { getRecommendedToolsOverRunInTerminal } from '../alternativeRecommendation.js';
|
||||
import { CommandLineAutoApprover, type IAutoApproveRule, type ICommandApprovalResult, type ICommandApprovalResultWithReason } from '../commandLineAutoApprover.js';
|
||||
import { CommandSimplifier } from '../commandSimplifier.js';
|
||||
import { BasicExecuteStrategy } from '../executeStrategy/basicExecuteStrategy.js';
|
||||
import type { ITerminalExecuteStrategy } from '../executeStrategy/executeStrategy.js';
|
||||
import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js';
|
||||
import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js';
|
||||
import { getOutput } from '../outputHelpers.js';
|
||||
import { dedupeRules, generateAutoApproveActions, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js';
|
||||
import { isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js';
|
||||
import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js';
|
||||
import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js';
|
||||
import { OutputMonitor } from './monitoring/outputMonitor.js';
|
||||
import { IPollingResult, OutputMonitorState } from './monitoring/types.js';
|
||||
import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js';
|
||||
import { type ICommandLineAnalyzer, type ICommandLineAnalyzerOptions } from './commandLineAnalyzer/commandLineAnalyzer.js';
|
||||
import { CommandLineAutoApproveAnalyzer } from './commandLineAnalyzer/commandLineAutoApproveAnalyzer.js';
|
||||
import { CommandLineFileWriteAnalyzer } from './commandLineAnalyzer/commandLineFileWriteAnalyzer.js';
|
||||
import { OutputMonitor } from './monitoring/outputMonitor.js';
|
||||
import { IPollingResult, OutputMonitorState } from './monitoring/types.js';
|
||||
|
||||
// #region Tool data
|
||||
|
||||
@@ -247,16 +244,6 @@ const telemetryIgnoredSequences = [
|
||||
'\x1b[O', // Focus out
|
||||
];
|
||||
|
||||
const promptInjectionWarningCommandsLower = [
|
||||
'curl',
|
||||
'wget',
|
||||
];
|
||||
const promptInjectionWarningCommandsLowerPwshOnly = [
|
||||
'invoke-restmethod',
|
||||
'invoke-webrequest',
|
||||
'irm',
|
||||
'iwr',
|
||||
];
|
||||
|
||||
export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
|
||||
@@ -265,7 +252,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
private readonly _treeSitterCommandParser: TreeSitterCommandParser;
|
||||
private readonly _telemetry: RunInTerminalToolTelemetry;
|
||||
private readonly _commandLineAnalyzers: ICommandLineAnalyzer[];
|
||||
protected readonly _commandLineAutoApprover: CommandLineAutoApprover;
|
||||
protected readonly _profileFetcher: TerminalProfileFetcher;
|
||||
protected readonly _sessionTerminalAssociations: Map<string, IToolTerminal> = new Map();
|
||||
|
||||
@@ -301,9 +287,9 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
this._commandSimplifier = _instantiationService.createInstance(CommandSimplifier, this._osBackend, this._treeSitterCommandParser);
|
||||
this._telemetry = _instantiationService.createInstance(RunInTerminalToolTelemetry);
|
||||
this._commandLineAnalyzers = [
|
||||
this._instantiationService.createInstance(CommandLineFileWriteAnalyzer, this._treeSitterCommandParser, (message, args) => this._logService.info(`CommandLineFileWriteAnalyzer: ${message}`, args)),
|
||||
this._register(this._instantiationService.createInstance(CommandLineFileWriteAnalyzer, this._treeSitterCommandParser, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineFileWriteAnalyzer: ${message}`, args))),
|
||||
this._register(this._instantiationService.createInstance(CommandLineAutoApproveAnalyzer, this._treeSitterCommandParser, this._telemetry, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineAutoApproveAnalyzer: ${message}`, args))),
|
||||
];
|
||||
this._commandLineAutoApprover = this._register(_instantiationService.createInstance(CommandLineAutoApprover));
|
||||
this._profileFetcher = _instantiationService.createInstance(TerminalProfileFetcher);
|
||||
|
||||
// Clear out warning accepted state if the setting is disabled
|
||||
@@ -334,9 +320,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
async prepareToolInvocation(context: IToolInvocationPreparationContext, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
|
||||
const args = context.parameters as IRunInTerminalInputParams;
|
||||
|
||||
const alternativeRecommendation = getRecommendedToolsOverRunInTerminal(args.command, this._languageModelToolsService);
|
||||
const presentation = alternativeRecommendation ? ToolInvocationPresentation.Hidden : undefined;
|
||||
|
||||
const os = await this._osBackend;
|
||||
const shell = await this._profileFetcher.getCopilotShell();
|
||||
const language = os === OperatingSystem.Windows ? 'pwsh' : 'sh';
|
||||
@@ -348,165 +331,74 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
if (toolEditedCommand === args.command) {
|
||||
toolEditedCommand = undefined;
|
||||
}
|
||||
const toolSpecificData: IChatTerminalToolInvocationData = {
|
||||
kind: 'terminal',
|
||||
terminalToolSessionId,
|
||||
commandLine: {
|
||||
original: args.command,
|
||||
toolEdited: toolEditedCommand
|
||||
},
|
||||
language,
|
||||
};
|
||||
|
||||
let autoApproveInfo: IMarkdownString | undefined;
|
||||
let confirmationMessages: IToolConfirmationMessages | undefined;
|
||||
// HACK: Exit early if there's an alternative recommendation, this is a little hacky but
|
||||
// it's the current mechanism for re-routing terminal tool calls to something else.
|
||||
const alternativeRecommendation = getRecommendedToolsOverRunInTerminal(args.command, this._languageModelToolsService);
|
||||
if (alternativeRecommendation) {
|
||||
confirmationMessages = undefined;
|
||||
} else {
|
||||
// Determine auto approval, this happens even when auto approve is off to that reasoning
|
||||
// can be reviewed in the terminal channel. It also allows gauging the effective set of
|
||||
// commands that would be auto approved if it were enabled.
|
||||
const actualCommand = toolEditedCommand ?? args.command;
|
||||
|
||||
let disclaimer: IMarkdownString | undefined;
|
||||
let customActions: ToolConfirmationAction[] | undefined;
|
||||
|
||||
const isAutoApproveEnabled = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnableAutoApprove) === true;
|
||||
const isAutoApproveWarningAccepted = this._storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false);
|
||||
const isAutoApproveAllowed = isAutoApproveEnabled && isAutoApproveWarningAccepted;
|
||||
let isAutoApproved = false;
|
||||
|
||||
let subCommands: string[] | undefined;
|
||||
const treeSitterLanguage = isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash;
|
||||
try {
|
||||
subCommands = await this._treeSitterCommandParser.extractSubCommands(treeSitterLanguage, actualCommand);
|
||||
this._logService.info(`RunInTerminalTool: autoApprove: Parsed sub-commands via ${treeSitterLanguage} grammar`, subCommands);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this._logService.info(`RunInTerminalTool: autoApprove: Failed to parse sub-commands via ${treeSitterLanguage} grammar`);
|
||||
}
|
||||
|
||||
const commandLineAnalyzerOptions: ICommandLineAnalyzerOptions = {
|
||||
instance,
|
||||
commandLine: actualCommand,
|
||||
os,
|
||||
shell,
|
||||
treeSitterLanguage,
|
||||
};
|
||||
const commandLineAnalyzerResults = await Promise.all(this._commandLineAnalyzers.map(e => e.analyze(commandLineAnalyzerOptions)));
|
||||
|
||||
if (subCommands) {
|
||||
const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, shell, os));
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(actualCommand);
|
||||
const autoApproveReasons: string[] = [
|
||||
...subCommandResults.map(e => e.reason),
|
||||
commandLineResult.reason,
|
||||
];
|
||||
|
||||
let isDenied = false;
|
||||
let autoApproveReason: 'subCommand' | 'commandLine' | undefined;
|
||||
let autoApproveDefault: boolean | undefined;
|
||||
|
||||
const deniedSubCommandResult = subCommandResults.find(e => e.result === 'denied');
|
||||
if (deniedSubCommandResult) {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Sub-command DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = deniedSubCommandResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'subCommand';
|
||||
} else if (commandLineResult.result === 'denied') {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Command line DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'commandLine';
|
||||
} else {
|
||||
if (subCommandResults.every(e => e.result === 'approved')) {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: All sub-commands auto-approved');
|
||||
autoApproveReason = 'subCommand';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = subCommandResults.every(e => e.rule?.isDefaultRule);
|
||||
} else {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: All sub-commands NOT auto-approved');
|
||||
if (commandLineResult.result === 'approved') {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Command line auto-approved');
|
||||
autoApproveReason = 'commandLine';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
} else {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Command line NOT auto-approved');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log detailed auto approval reasoning
|
||||
for (const reason of autoApproveReasons) {
|
||||
this._logService.info(`RunInTerminalTool: autoApprove: - ${reason}`);
|
||||
}
|
||||
|
||||
// Apply auto approval or force it off depending on enablement/opt-in state
|
||||
if (isAutoApproveEnabled && commandLineAnalyzerResults.every(r => r.isAutoApproveAllowed)) {
|
||||
autoApproveInfo = this._createAutoApproveInfo(
|
||||
isAutoApproved,
|
||||
isDenied,
|
||||
autoApproveReason,
|
||||
subCommandResults,
|
||||
commandLineResult,
|
||||
);
|
||||
} else {
|
||||
isAutoApproved = false;
|
||||
}
|
||||
|
||||
// Send telemetry about auto approval process
|
||||
this._telemetry.logPrepare({
|
||||
terminalToolSessionId,
|
||||
subCommands,
|
||||
autoApproveAllowed: !isAutoApproveEnabled ? 'off' : isAutoApproveWarningAccepted ? 'allowed' : 'needsOptIn',
|
||||
autoApproveResult: isAutoApproved ? 'approved' : isDenied ? 'denied' : 'manual',
|
||||
autoApproveReason,
|
||||
autoApproveDefault
|
||||
});
|
||||
|
||||
// Add disclaimers for various security concerns
|
||||
const disclaimers: string[] = [];
|
||||
disclaimers.push(...commandLineAnalyzerResults.map(e => e.disclaimers).flat());
|
||||
|
||||
// Prompt injection warning for common commands that return content from the web
|
||||
const subCommandsLowerFirstWordOnly = subCommands.map(command => command.split(' ')[0].toLowerCase());
|
||||
if (!isAutoApproved && (
|
||||
subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLower.includes(command)) ||
|
||||
(isPowerShell(shell, os) && subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLowerPwshOnly.includes(command)))
|
||||
)) {
|
||||
disclaimers.push(localize('runInTerminal.promptInjectionDisclaimer', 'Web content may contain malicious code or attempt prompt injection attacks.'));
|
||||
}
|
||||
|
||||
// Combine disclaimers
|
||||
if (disclaimers.length > 0) {
|
||||
disclaimer = new MarkdownString(`$(${Codicon.info.id}) ` + disclaimers.join(' '), { supportThemeIcons: true });
|
||||
}
|
||||
|
||||
if (!isAutoApproved && isAutoApproveEnabled) {
|
||||
customActions = generateAutoApproveActions(actualCommand, subCommands, { subCommandResults, commandLineResult });
|
||||
}
|
||||
}
|
||||
|
||||
let shellType = basename(shell, '.exe');
|
||||
if (shellType === 'powershell') {
|
||||
shellType = 'pwsh';
|
||||
}
|
||||
confirmationMessages = (isAutoApproved && isAutoApproveAllowed && commandLineAnalyzerResults.every(r => r.isAutoApproveAllowed)) ? undefined : {
|
||||
title: args.isBackground
|
||||
? localize('runInTerminal.background', "Run `{0}` command? (background terminal)", shellType)
|
||||
: localize('runInTerminal', "Run `{0}` command?", shellType),
|
||||
message: new MarkdownString(args.explanation),
|
||||
disclaimer,
|
||||
terminalCustomActions: customActions,
|
||||
toolSpecificData.alternativeRecommendation = alternativeRecommendation;
|
||||
return {
|
||||
confirmationMessages: undefined,
|
||||
presentation: ToolInvocationPresentation.Hidden,
|
||||
toolSpecificData,
|
||||
};
|
||||
}
|
||||
|
||||
// Determine auto approval, this happens even when auto approve is off to that reasoning
|
||||
// can be reviewed in the terminal channel. It also allows gauging the effective set of
|
||||
// commands that would be auto approved if it were enabled.
|
||||
const commandLine = toolEditedCommand ?? args.command;
|
||||
|
||||
const isAutoApproveEnabled = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnableAutoApprove) === true;
|
||||
const isAutoApproveWarningAccepted = this._storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false);
|
||||
const isAutoApproveAllowed = isAutoApproveEnabled && isAutoApproveWarningAccepted;
|
||||
|
||||
const commandLineAnalyzerOptions: ICommandLineAnalyzerOptions = {
|
||||
instance,
|
||||
commandLine,
|
||||
os,
|
||||
shell,
|
||||
treeSitterLanguage: isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash,
|
||||
terminalToolSessionId,
|
||||
};
|
||||
const commandLineAnalyzerResults = await Promise.all(this._commandLineAnalyzers.map(e => e.analyze(commandLineAnalyzerOptions)));
|
||||
|
||||
const disclaimersRaw = commandLineAnalyzerResults.filter(e => e.disclaimers).flatMap(e => e.disclaimers);
|
||||
let disclaimer: IMarkdownString | undefined;
|
||||
if (disclaimersRaw.length > 0) {
|
||||
disclaimer = new MarkdownString(`$(${Codicon.info.id}) ` + disclaimersRaw.join(' '), { supportThemeIcons: true });
|
||||
}
|
||||
|
||||
const customActions = commandLineAnalyzerResults.map(e => e.customActions ?? []).flat();
|
||||
toolSpecificData.autoApproveInfo = commandLineAnalyzerResults.find(e => e.autoApproveInfo)?.autoApproveInfo;
|
||||
|
||||
let shellType = basename(shell, '.exe');
|
||||
if (shellType === 'powershell') {
|
||||
shellType = 'pwsh';
|
||||
}
|
||||
|
||||
const isFinalAutoApproved = isAutoApproveAllowed && commandLineAnalyzerResults.every(e => e.isAutoApproveAllowed);
|
||||
const confirmationMessages = isFinalAutoApproved ? undefined : {
|
||||
title: args.isBackground
|
||||
? localize('runInTerminal.background', "Run `{0}` command? (background terminal)", shellType)
|
||||
: localize('runInTerminal', "Run `{0}` command?", shellType),
|
||||
message: new MarkdownString(args.explanation),
|
||||
disclaimer,
|
||||
terminalCustomActions: customActions,
|
||||
};
|
||||
|
||||
return {
|
||||
confirmationMessages,
|
||||
presentation,
|
||||
toolSpecificData: {
|
||||
kind: 'terminal',
|
||||
terminalToolSessionId,
|
||||
commandLine: {
|
||||
original: args.command,
|
||||
toolEdited: toolEditedCommand
|
||||
},
|
||||
language,
|
||||
alternativeRecommendation,
|
||||
autoApproveInfo,
|
||||
}
|
||||
toolSpecificData,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -922,78 +814,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Auto approve
|
||||
|
||||
private _createAutoApproveInfo(
|
||||
isAutoApproved: boolean,
|
||||
isDenied: boolean,
|
||||
autoApproveReason: 'subCommand' | 'commandLine' | undefined,
|
||||
subCommandResults: ICommandApprovalResultWithReason[],
|
||||
commandLineResult: ICommandApprovalResultWithReason,
|
||||
): MarkdownString | undefined {
|
||||
const formatRuleLinks = (result: SingleOrMany<{ result: ICommandApprovalResult; rule?: IAutoApproveRule; reason: string }>): string => {
|
||||
return asArray(result).map(e => {
|
||||
const settingsUri = createCommandUri(openTerminalSettingsLinkCommandId, e.rule!.sourceTarget);
|
||||
return `[\`${e.rule!.sourceText}\`](${settingsUri.toString()} "${localize('ruleTooltip', 'View rule in settings')}")`;
|
||||
}).join(', ');
|
||||
};
|
||||
|
||||
const mdTrustSettings = {
|
||||
isTrusted: {
|
||||
enabledCommands: [openTerminalSettingsLinkCommandId]
|
||||
}
|
||||
};
|
||||
|
||||
const config = this._configurationService.inspect<boolean | Record<string, boolean>>(ChatConfiguration.GlobalAutoApprove);
|
||||
const isGlobalAutoApproved = config?.value ?? config.defaultValue;
|
||||
if (isGlobalAutoApproved) {
|
||||
const settingsUri = createCommandUri(openTerminalSettingsLinkCommandId, 'global');
|
||||
return new MarkdownString(`${localize('autoApprove.global', 'Auto approved by setting {0}', `[\`${ChatConfiguration.GlobalAutoApprove}\`](${settingsUri.toString()} "${localize('ruleTooltip.global', 'View settings')}")`)}`, mdTrustSettings);
|
||||
}
|
||||
|
||||
if (isAutoApproved) {
|
||||
switch (autoApproveReason) {
|
||||
case 'commandLine': {
|
||||
if (commandLineResult.rule) {
|
||||
return new MarkdownString(localize('autoApprove.rule', 'Auto approved by rule {0}', formatRuleLinks(commandLineResult)), mdTrustSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'subCommand': {
|
||||
const uniqueRules = dedupeRules(subCommandResults);
|
||||
if (uniqueRules.length === 1) {
|
||||
return new MarkdownString(localize('autoApprove.rule', 'Auto approved by rule {0}', formatRuleLinks(uniqueRules)), mdTrustSettings);
|
||||
} else if (uniqueRules.length > 1) {
|
||||
return new MarkdownString(localize('autoApprove.rules', 'Auto approved by rules {0}', formatRuleLinks(uniqueRules)), mdTrustSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (isDenied) {
|
||||
switch (autoApproveReason) {
|
||||
case 'commandLine': {
|
||||
if (commandLineResult.rule) {
|
||||
return new MarkdownString(localize('autoApproveDenied.rule', 'Auto approval denied by rule {0}', formatRuleLinks(commandLineResult)), mdTrustSettings);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'subCommand': {
|
||||
const uniqueRules = dedupeRules(subCommandResults.filter(e => e.result === 'denied'));
|
||||
if (uniqueRules.length === 1) {
|
||||
return new MarkdownString(localize('autoApproveDenied.rule', 'Auto approval denied by rule {0}', formatRuleLinks(uniqueRules)));
|
||||
} else if (uniqueRules.length > 1) {
|
||||
return new MarkdownString(localize('autoApproveDenied.rules', 'Auto approval denied by rules {0}', formatRuleLinks(uniqueRules)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
class BackgroundTerminalExecution extends Disposable {
|
||||
|
||||
@@ -36,7 +36,6 @@ import { arch } from '../../../../../../base/common/process.js';
|
||||
class TestRunInTerminalTool extends RunInTerminalTool {
|
||||
protected override _osBackend: Promise<OperatingSystem> = Promise.resolve(OperatingSystem.Windows);
|
||||
|
||||
get commandLineAutoApprover() { return this._commandLineAutoApprover; }
|
||||
get sessionTerminalAssociations() { return this._sessionTerminalAssociations; }
|
||||
get profileFetcher() { return this._profileFetcher; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user