commandAutoApprover.ts ×14

Frontier kind: Code frontier

unlabeled · c_bfa398dcd02a

553 tests · 8277 LOC · 40 files · introduces 0 tests · 303 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
14 ranges303 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1294 ranges8277 lines · 40 files · Browse complete extent
All tests (intent)
553 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 303 introduced LOC across 14 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/commandAutoApprover.ts 303 introduced LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- commandAutoApprover.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { Language, Parser, Query, QueryCapture } from '@vscode/tree-sitter-wasm';
7 > import * as fs from 'fs';
8 > import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
9 > import { FileAccess } from '../../../base/common/network.js';
10 > import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { getAppNodeModulesPath } from './appNodeModules.js';
13 > import { ILogService } from '../../log/common/log.js';
14 > import type { AgentHostTerminalAutoApproveRuleValue, AgentHostTerminalAutoApproveRules } from '../common/agentHostSchema.js';
15 >
16 > /**
17 > * Redirect destinations that do not result in a write to an arbitrary file
18 > * on disk: the /dev sinks that discard output (`/dev/null`) or write back to
19 > * the same terminal (`/dev/stdout`, `/dev/stderr`, `/dev/tty`).
20 > */
21 > const SAFE_REDIRECT_TARGETS: ReadonlySet<string> = new Set([
22 > '/dev/null',
23 > '/dev/stdout',
24 > '/dev/stderr',
25 > '/dev/tty',
26 > ]);
27 >
28 > /**
29 > * Returns true when the given redirection destination is known to be safe:
30 > * either a known-safe /dev sink or a file-descriptor duplication target
31 > * like `&1` (used in `2>&1`).
32 > */
33 function isSafeRedirectDestination(dest: string): boolean {
34 let cleaned = dest.trim();
46 return SAFE_REDIRECT_TARGETS.has(cleaned);
47 }
49 > /**
50 > * Classification of a tree-sitter `file_redirect` node.
51 > * - `read`: input-only redirect (`<`, `<&N`) — never writes.
52 > * - `safeWrite`: write to a known-safe sink (`/dev/null`, fd duplication, ...).
53 > * - `unsafeWrite`: write to an arbitrary destination. The destination string
54 > * (with surrounding quotes stripped) is included when it could be parsed,
55 > * so the caller may decide whether the target is acceptable.
56 > */
57 > type FileRedirectClassification =
58 > | { kind: 'read' }
59 > | { kind: 'safeWrite' }
60 > | { kind: 'unsafeWrite'; dest: string | undefined };
61 >
62 function classifyFileRedirect(redirectText: string): FileRedirectClassification {
63 if (!redirectText.includes('>')) {
79 return { kind: 'unsafeWrite', dest };
80 }
82 > /**
83 > * Result of a command auto-approval check.
84 > * - `approved`: all sub-commands match allow rules and none are denied
85 > * - `denied`: at least one sub-command matches a deny rule
86 > * - `noMatch`: no rule matched — requires user confirmation
87 > */
88 > export type CommandApprovalResult = 'approved' | 'denied' | 'noMatch';
89 >
90 > /** Options for {@link CommandAutoApprover.shouldAutoApprove}. */
91 > export interface IShouldAutoApproveOptions {
92 > /**
93 > * Predicate that decides whether a write redirection to the given
94 > * destination is acceptable. Called once per write-redirect destination
95 > * found in the command line; the destination is the raw string the user
96 > * typed (with surrounding quotes stripped). The predicate is responsible
97 > * for resolving relative paths and applying its own policy.
98 > *
99 > * When omitted, any write redirect to a destination outside the known-safe
100 > * sinks (e.g. `/dev/null`) downgrades the result to `noMatch`.
101 > */
102 > readonly isWriteDestApproved?: (dest: string) => boolean;
103 > /**
104 > * Effective VS Code `chat.tools.terminal.autoApprove` rules forwarded from
105 > * the renderer. When omitted, the agent host falls back to its bundled
106 > * default rules for compatibility with older clients.
107 > */
108 > readonly autoApproveRules?: AgentHostTerminalAutoApproveRules;
109 > }
110 >
111 > interface IAutoApproveRule {
112 > readonly regex: RegExp;
113 > }
114 >
115 > interface IAutoApproveRules {
116 > readonly allowRules: IAutoApproveRule[];
117 > readonly denyRules: IAutoApproveRule[];
118 > readonly allowCommandLineRules: IAutoApproveRule[];
119 > readonly denyCommandLineRules: IAutoApproveRule[];
120 > }
121 >
122 > const neverMatchRegex = /(?!.*)/;
123 > const transientEnvVarRegex = /^[A-Z_][A-Z0-9_]*=/i;
124 >
125 > /**
126 > * Auto-approves or denies shell commands based on terminal auto-approve rules.
127 > *
128 > * Uses tree-sitter to parse compound commands (`foo && bar`) into
129 > * sub-commands that are individually checked against allow/deny lists.
130 > * The rules are normally forwarded from VS Code's
131 > * `chat.tools.terminal.autoApprove` setting. A bundled default table is kept
132 > * as a compatibility fallback for clients that have not forwarded rules yet.
133 > *
134 > * Tree-sitter is initialized eagerly; call {@link initialize} and await the
135 > * result before using {@link shouldAutoApprove} to guarantee synchronous
136 > * parsing. If tree-sitter fails to load or parse the command,
137 > * {@link shouldAutoApprove} returns `noMatch` so the user is prompted for
138 > * confirmation rather than auto-approving based on the command name alone.
139 > */
140 > export class CommandAutoApprover extends Disposable {
141 >
142 > private _fallbackRules: IAutoApproveRules | undefined;
143 > private _cachedRuleConfig: AgentHostTerminalAutoApproveRules | undefined;
144 > private _cachedRules: IAutoApproveRules | undefined;
145 > private _parser: Parser | undefined;
146 > private _bashLanguage: Language | undefined;
147 > private _queryClass: typeof Query | undefined;
148 > private readonly _initPromise: Promise<void>;
149 >
150 > constructor(
151 private readonly _logService: ILogService,
152 ) {
154 this._initPromise = this._initTreeSitter();
155 }
157 > /**
158 > * Returns a promise that resolves once tree-sitter WASM has been loaded.
159 > * Await this before processing any events to guarantee that
160 > * {@link shouldAutoApprove} can parse commands synchronously.
161 > */
162 > initialize(): Promise<void> {
163 return this._initPromise;
164 }
166 > /**
167 > * Synchronously check whether the given command line should be auto-approved.
168 > * Uses tree-sitter (if loaded) to parse compound commands into sub-commands.
169 > *
170 > * When the command contains write redirections, `options.isWriteDestApproved`
171 > * is consulted for each destination. If every destination is approved by the
172 > * predicate, write redirections do not block auto-approval.
173 > */
174 > shouldAutoApprove(commandLine: string, options?: IShouldAutoApproveOptions): CommandApprovalResult {
175 const trimmed = commandLine.trimStart();
176 if (trimmed.length === 0) {
204 return result;
205 }
207 > private _matchSubCommands(subCommands: string[], rules: IAutoApproveRules): CommandApprovalResult {
208 let allApproved = true;
209 for (const subCommand of subCommands) {
223 return allApproved ? 'approved' : 'noMatch';
224 }
226 > private _matchSingleCommand(command: string, rules: IAutoApproveRules): CommandApprovalResult {
227 // Check deny rules first
228 if (this._matchesRule(command, rules.denyRules)) {
237 return 'noMatch';
238 }
240 > private _matchesRule(command: string, rules: readonly IAutoApproveRule[]): boolean {
241 for (const rule of rules) {
242 if (rule.regex.test(command)) {
246 return false;
247 }
249 > // ---- Tree-sitter --------------------------------------------------------
250 >
251 > private _extractSubCommands(commandLine: string): { subCommands: string[]; unsafeWriteDests: (string | undefined)[] } | undefined {
252 if (!this._parser || !this._bashLanguage || !this._queryClass) {
253 return undefined;
291 }
292 }
294 > private async _initTreeSitter(): Promise<void> {
295 try {
296 const { default: TreeSitter } = (await import('@vscode/tree-sitter-wasm'));
348 }
349 }
351 > // ---- Rules --------------------------------------------------------------
352 >
353 > private _compileRules(ruleConfig: AgentHostTerminalAutoApproveRules | undefined): IAutoApproveRules {
354 if (!ruleConfig) {
355 if (!this._fallbackRules) {
367 return this._cachedRules;
368 }
370 > private _compileRuleEntries(ruleConfig: Readonly<Record<string, AgentHostTerminalAutoApproveRuleValue>>): IAutoApproveRules {
371 const allowRules: IAutoApproveRule[] = [];
372 const denyRules: IAutoApproveRule[] = [];
399 return { allowRules, denyRules, allowCommandLineRules, denyCommandLineRules };
400 }
402 >
403 > // ---- Regex conversion -------------------------------------------------------
404 >
405 function convertAutoApproveEntryToRegex(value: string): RegExp {
406 // If wrapped in `/`, treat as regex
446 return new RegExp(`^${sanitizedValue}\\b`);
447 }
449 > // ---- Default rules ----------------------------------------------------------
450 > //
451 > // Compatibility fallback for clients that do not forward the VS Code
452 > // `chat.tools.terminal.autoApprove` setting.
453 > // TODO: Remove this fallback once all agent-host clients are guaranteed to
454 > // forward `chat.tools.terminal.autoApprove` before shell approvals run.
455 >
456 > const DEFAULT_TERMINAL_AUTO_APPROVE_RULES: Readonly<Record<string, AgentHostTerminalAutoApproveRuleValue>> = {
457 > // Safe readonly commands
458 > cd: true,
459 > echo: true,
460 > ls: true,
461 > dir: true,
462 > pwd: true,
463 > cat: true,
464 > head: true,
465 > tail: true,
466 > findstr: true,
467 > wc: true,
468 > tr: true,
469 > cut: true,
470 > cmp: true,
471 > which: true,
472 > basename: true,
473 > dirname: true,
474 > realpath: true,
475 > readlink: true,
476 > stat: true,
477 > file: true,
478 > od: true,
479 > du: true,
480 > df: true,
481 > sleep: true,
482 > nl: true,
483 >
484 > grep: true,
485 >
486 > // Safe git sub-commands
487 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+status\\b/': true,
488 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b/': true,
489 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b.*\\s--output(=|\\s|$)/': false,
490 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+show\\b/': true,
491 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+diff\\b/': true,
492 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+ls-files\\b/': true,
493 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+grep\\b/': true,
494 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b/': true,
495 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b.*\\s-(d|D|m|M|-delete|-force)\\b/': false,
496 >
497 > // Docker readonly sub-commands
498 > '/^docker\\s+(ps|images|info|version|inspect|logs|top|stats|port|diff|search|events)\\b/': true,
499 > '/^docker\\s+(container|image|network|volume|context|system)\\s+(ls|ps|inspect|history|show|df|info)\\b/': true,
500 > '/^docker\\s+compose\\s+(ps|ls|top|logs|images|config|version|port|events)\\b/': true,
501 >
502 > // PowerShell
503 > 'Get-ChildItem': true,
504 > 'Get-Content': true,
505 > 'Get-Date': true,
506 > 'Get-Random': true,
507 > 'Get-Location': true,
508 > 'Set-Location': true,
509 > 'Write-Host': true,
510 > 'Write-Output': true,
511 > 'Out-String': true,
512 > 'Split-Path': true,
513 > 'Join-Path': true,
514 > 'Start-Sleep': true,
515 > 'Where-Object': true,
516 > '/^Select-[a-z0-9]/i': true,
517 > '/^Measure-[a-z0-9]/i': true,
518 > '/^Compare-[a-z0-9]/i': true,
519 > '/^Format-[a-z0-9]/i': true,
520 > '/^Sort-[a-z0-9]/i': true,
521 >
522 > // Package manager read-only commands
523 > '/^npm\\s+(ls|list|outdated|view|info|show|explain|why|root|prefix|bin|search|doctor|fund|repo|bugs|docs|home|help(-search)?)\\b/': true,
524 > '/^npm\\s+config\\s+(list|get)\\b/': true,
525 > '/^npm\\s+pkg\\s+get\\b/': true,
526 > '/^npm\\s+audit$/': true,
527 > '/^npm\\s+cache\\s+verify\\b/': true,
528 > '/^yarn\\s+(list|outdated|info|why|bin|help|versions)\\b/': true,
529 > '/^yarn\\s+licenses\\b/': true,
530 > '/^yarn\\s+audit\\b(?!.*\\bfix\\b)/': true,
531 > '/^yarn\\s+config\\s+(list|get)\\b/': true,
532 > '/^yarn\\s+cache\\s+dir\\b/': true,
533 > '/^pnpm\\s+(ls|list|outdated|why|root|bin|doctor)\\b/': true,
534 > '/^pnpm\\s+licenses\\b/': true,
535 > '/^pnpm\\s+audit\\b(?!.*\\bfix\\b)/': true,
536 > '/^pnpm\\s+config\\s+(list|get)\\b/': true,
537 >
538 > // Safe lockfile-only installs
539 > 'npm ci': true,
540 > '/^yarn\\s+install\\s+--frozen-lockfile\\b/': true,
541 > '/^pnpm\\s+install\\s+--frozen-lockfile\\b/': true,
542 >
543 > // Safe commands with dangerous arg blocking
544 > column: true,
545 > '/^column\\b.*\\s-c\\s+[0-9]{4,}/': false,
546 > date: true,
547 > '/^date\\b.*\\s(-s|--set)\\b/': false,
548 > find: true,
549 > '/^find\\b.*\\s-(delete|exec|execdir|fprint|fprintf|fls|ok|okdir)\\b/': false,
550 > rg: true,
551 > '/^rg\\b.*\\s(--pre|--hostname-bin)\\b/': false,
552 > sed: true,
553 > '/^sed\\b.*\\s(-[a-zA-Z]*(e|f)[a-zA-Z]*|--expression|--file)\\b/': false,
554 > '/^sed\\b.*s\\/.*\\/.*\\/[ew]/': false,
555 > '/^sed\\b.*;W/': false,
556 > sort: true,
557 > '/^sort\\b.*\\s-(o|S)\\b/': false,
558 > tree: true,
559 > '/^tree\\b.*\\s-o\\b/': false,
560 > '/^xxd$/': true,
561 > '/^xxd\\b(\\s+-\\S+)*\\s+[^-\\s]\\S*$/': true,
562 >
563 > // Dangerous commands
564 > rm: false,
565 > rmdir: false,
566 > del: false,
567 > 'Remove-Item': false,
568 > ri: false,
569 > rd: false,
570 > erase: false,
571 > dd: false,
572 > kill: false,
573 > ps: false,
574 > top: false,
575 > 'Stop-Process': false,
576 > spps: false,
577 > taskkill: false,
578 > 'taskkill.exe': false,
579 > curl: false,
580 > wget: false,
581 > 'Invoke-RestMethod': false,
582 > 'Invoke-WebRequest': false,
583 > irm: false,
584 > iwr: false,
585 > chmod: false,
586 > chown: false,
587 > 'Set-ItemProperty': false,
588 > sp: false,
589 > 'Set-Acl': false,
590 > jq: false,
591 > xargs: false,
592 > eval: false,
593 > 'Invoke-Expression': false,
594 > iex: false,
595 > };