diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 6e2c39552619f..da58e2d5ba4df 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -11,7 +11,8 @@ import { escapeRegExpCharacters, removeAnsiEscapeCodes } from '../../../../../ba import { localize } from '../../../../../nls.js'; import type { TerminalNewAutoApproveButtonData } from '../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import type { ToolConfirmationAction } from '../../../chat/common/tools/languageModelToolsService.js'; -import type { ICommandApprovalResultWithReason } from './commandLineAutoApprover.js'; +import type { ICommandApprovalResultWithReason } from './tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.js'; +import { isAutoApproveRule } from './tools/commandLineAnalyzer/commandLineAnalyzer.js'; export function isPowerShell(envShell: string, os: OperatingSystem): boolean { if (os === OperatingSystem.Windows) { @@ -262,6 +263,10 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str export function dedupeRules(rules: ICommandApprovalResultWithReason[]): ICommandApprovalResultWithReason[] { return rules.filter((result, index, array) => { - return result.rule && array.findIndex(r => r.rule && r.rule.sourceText === result.rule!.sourceText) === index; + if (!isAutoApproveRule(result.rule)) { + return false; + } + const sourceText = result.rule.sourceText; + return array.findIndex(r => isAutoApproveRule(r.rule) && r.rule.sourceText === sourceText) === index; }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts similarity index 88% rename from src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts rename to src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts index 7873379b95520..effcd105dbc50 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts @@ -3,28 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import type { OperatingSystem } from '../../../../../base/common/platform.js'; -import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../../../base/common/strings.js'; -import { isObject } from '../../../../../base/common/types.js'; -import { structuralEquals } from '../../../../../base/common/equals.js'; -import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; -import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; -import { isPowerShell } from './runInTerminalHelpers.js'; -import { ITerminalChatService } from '../../../terminal/browser/terminal.js'; - -export interface IAutoApproveRule { - regex: RegExp; - regexCaseInsensitive: RegExp; - sourceText: string; - sourceTarget: ConfigurationTarget | 'session'; - isDefaultRule: boolean; -} +import { structuralEquals } from '../../../../../../../../base/common/equals.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import type { OperatingSystem } from '../../../../../../../../base/common/platform.js'; +import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../../../../../../base/common/strings.js'; +import { isObject } from '../../../../../../../../base/common/types.js'; +import type { URI } from '../../../../../../../../base/common/uri.js'; +import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../../../platform/instantiation/common/instantiation.js'; +import { ITerminalChatService } from '../../../../../../terminal/browser/terminal.js'; +import { TerminalChatAgentToolsSettingId } from '../../../../common/terminalChatAgentToolsConfiguration.js'; +import { isPowerShell } from '../../../runInTerminalHelpers.js'; +import type { IAutoApproveRule, INpmScriptAutoApproveRule } from '../commandLineAnalyzer.js'; +import { NpmScriptAutoApprover } from './npmScriptAutoApprover.js'; export interface ICommandApprovalResultWithReason { result: ICommandApprovalResult; reason: string; - rule?: IAutoApproveRule; + rule?: IAutoApproveRule | INpmScriptAutoApproveRule; } export type ICommandApprovalResult = 'approved' | 'denied' | 'noMatch'; @@ -37,12 +33,15 @@ export class CommandLineAutoApprover extends Disposable { private _allowListRules: IAutoApproveRule[] = []; private _allowListCommandLineRules: IAutoApproveRule[] = []; private _denyListCommandLineRules: IAutoApproveRule[] = []; + private readonly _npmScriptAutoApprover: NpmScriptAutoApprover; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, ) { super(); + this._npmScriptAutoApprover = this._register(instantiationService.createInstance(NpmScriptAutoApprover)); this.updateConfiguration(); this._register(this._configurationService.onDidChangeConfiguration(e => { if ( @@ -78,7 +77,7 @@ export class CommandLineAutoApprover extends Disposable { this._denyListCommandLineRules = denyListCommandLineRules; } - isCommandAutoApproved(command: string, shell: string, os: OperatingSystem, chatSessionId?: string): ICommandApprovalResultWithReason { + async isCommandAutoApproved(command: string, shell: string, os: OperatingSystem, cwd: URI | undefined, chatSessionId?: string): Promise { // Check if the command has a transient environment variable assignment prefix which we // always deny for now as it can easily lead to execute other commands if (transientEnvVarRegex.test(command)) { @@ -121,6 +120,16 @@ export class CommandLineAutoApprover extends Disposable { } } + // Check if this is an npm/yarn/pnpm script defined in package.json + const npmScriptResult = await this._npmScriptAutoApprover.isCommandAutoApproved(command, cwd); + if (npmScriptResult.isAutoApproved) { + return { + result: 'approved', + rule: { type: 'npmScript', npmScriptResult }, + reason: `Command '${command}' is approved as npm script '${npmScriptResult.scriptName}' is defined in package.json` + }; + } + // TODO: LLM-based auto-approval https://github.com/microsoft/vscode/issues/253267 // Fallback is always to require approval diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts new file mode 100644 index 0000000000000..57da0c5439c13 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts @@ -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 { MarkdownString, type IMarkdownString } from '../../../../../../../../base/common/htmlContent.js'; +import { visit, type JSONVisitor } from '../../../../../../../../base/common/json.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { IUriIdentityService } from '../../../../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { localize } from '../../../../../../../../nls.js'; +import { IConfigurationService } from '../../../../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import { IWorkspaceContextService, type IWorkspaceFolder } from '../../../../../../../../platform/workspace/common/workspace.js'; +import { TerminalChatAgentToolsSettingId } from '../../../../common/terminalChatAgentToolsConfiguration.js'; + +/** + * Regex patterns to match npm/yarn/pnpm run commands and extract the script name. + * Uses named capture groups: 'command' for the package manager, 'scriptName' for the script. + */ +const npmRunPatterns = [ + // npm run