Ensure session rules only apply to a single session

This commit is contained in:
Daniel Imms
2025-12-18 07:05:55 -08:00
parent c943585d55
commit 6bcd25a25a
5 changed files with 43 additions and 20 deletions

View File

@@ -266,8 +266,9 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS
const userRules = newRules.filter(r => r.scope === 'user'); const userRules = newRules.filter(r => r.scope === 'user');
// Handle session-scoped rules (temporary, in-memory only) // Handle session-scoped rules (temporary, in-memory only)
const chatSessionId = this.context.element.sessionId;
for (const rule of sessionRules) { for (const rule of sessionRules) {
this.terminalChatService.addSessionAutoApproveRule(rule.key, rule.value); this.terminalChatService.addSessionAutoApproveRule(chatSessionId, rule.key, rule.value);
} }
// Handle workspace-scoped rules // Handle workspace-scoped rules

View File

@@ -220,16 +220,18 @@ export interface ITerminalChatService {
/** /**
* Add a session-scoped auto-approve rule. * Add a session-scoped auto-approve rule.
* @param chatSessionId The chat session ID to associate the rule with
* @param key The rule key (command or regex pattern) * @param key The rule key (command or regex pattern)
* @param value The rule value (approval boolean or object with approve and matchCommandLine) * @param value The rule value (approval boolean or object with approve and matchCommandLine)
*/ */
addSessionAutoApproveRule(key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void; addSessionAutoApproveRule(chatSessionId: string, key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void;
/** /**
* Get all session-scoped auto-approve rules. * Get all session-scoped auto-approve rules for a specific chat session.
* @returns A record of all session-scoped auto-approve rules * @param chatSessionId The chat session ID to get rules for
* @returns A record of all session-scoped auto-approve rules for the session
*/ */
getSessionAutoApproveRules(): Readonly<Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>>; getSessionAutoApproveRules(chatSessionId: string): Readonly<Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>>;
} }
/** /**

View File

@@ -54,10 +54,11 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ
private readonly _sessionAutoApprovalEnabled = new Set<string>(); private readonly _sessionAutoApprovalEnabled = new Set<string>();
/** /**
* Tracks session-scoped auto-approve rules. These are temporary rules that last only for the * Tracks session-scoped auto-approve rules per chat session. These are temporary rules that
* duration of the VS Code session (not persisted to disk). * last only for the duration of the chat session (not persisted to disk).
* Map<chatSessionId, Record<ruleKey, ruleValue>>
*/ */
private readonly _sessionAutoApproveRules: Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }> = {}; private readonly _sessionAutoApproveRules = new Map<string, Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>>();
constructor( constructor(
@ILogService private readonly _logService: ILogService, @ILogService private readonly _logService: ILogService,
@@ -72,6 +73,16 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ
this._hasHiddenToolTerminalContext = TerminalChatContextKeys.hasHiddenChatTerminals.bindTo(this._contextKeyService); this._hasHiddenToolTerminalContext = TerminalChatContextKeys.hasHiddenChatTerminals.bindTo(this._contextKeyService);
this._restoreFromStorage(); this._restoreFromStorage();
// Clear session auto-approve rules when chat sessions end
this._register(this._chatService.onDidDisposeSession(e => {
for (const resource of e.sessionResource) {
const sessionId = LocalChatSessionUri.parseLocalSessionId(resource);
if (sessionId) {
this._sessionAutoApproveRules.delete(sessionId);
}
}
}));
} }
registerTerminalInstanceWithToolSession(terminalToolSessionId: string | undefined, instance: ITerminalInstance): void { registerTerminalInstanceWithToolSession(terminalToolSessionId: string | undefined, instance: ITerminalInstance): void {
@@ -320,11 +331,16 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ
return this._sessionAutoApprovalEnabled.has(chatSessionId); return this._sessionAutoApprovalEnabled.has(chatSessionId);
} }
addSessionAutoApproveRule(key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void { addSessionAutoApproveRule(chatSessionId: string, key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void {
this._sessionAutoApproveRules[key] = value; let sessionRules = this._sessionAutoApproveRules.get(chatSessionId);
if (!sessionRules) {
sessionRules = {};
this._sessionAutoApproveRules.set(chatSessionId, sessionRules);
}
sessionRules[key] = value;
} }
getSessionAutoApproveRules(): Readonly<Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>> { getSessionAutoApproveRules(chatSessionId: string): Readonly<Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>> {
return this._sessionAutoApproveRules; return this._sessionAutoApproveRules.get(chatSessionId) ?? {};
} }
} }

View File

@@ -85,7 +85,7 @@ export class CommandLineAutoApprover extends Disposable {
this._denyListCommandLineRules = denyListCommandLineRules; this._denyListCommandLineRules = denyListCommandLineRules;
} }
isCommandAutoApproved(command: string, shell: string, os: OperatingSystem): ICommandApprovalResultWithReason { isCommandAutoApproved(command: string, shell: string, os: OperatingSystem, chatSessionId?: string): ICommandApprovalResultWithReason {
// Check if the command has a transient environment variable assignment prefix which we // 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 // always deny for now as it can easily lead to execute other commands
if (transientEnvVarRegex.test(command)) { if (transientEnvVarRegex.test(command)) {
@@ -107,7 +107,7 @@ export class CommandLineAutoApprover extends Disposable {
} }
// Check session allow rules (session deny rules can't exist) // Check session allow rules (session deny rules can't exist)
for (const rule of this._getSessionRules().allowListRules) { for (const rule of this._getSessionRules(chatSessionId).allowListRules) {
if (this._commandMatchesRule(rule, command, shell, os)) { if (this._commandMatchesRule(rule, command, shell, os)) {
return { return {
result: 'approved', result: 'approved',
@@ -137,7 +137,7 @@ export class CommandLineAutoApprover extends Disposable {
}; };
} }
isCommandLineAutoApproved(commandLine: string): ICommandApprovalResultWithReason { isCommandLineAutoApproved(commandLine: string, chatSessionId?: string): ICommandApprovalResultWithReason {
// Check the config deny list first to see if this command line requires explicit approval // Check the config deny list first to see if this command line requires explicit approval
for (const rule of this._denyListCommandLineRules) { for (const rule of this._denyListCommandLineRules) {
if (rule.regex.test(commandLine)) { if (rule.regex.test(commandLine)) {
@@ -150,7 +150,7 @@ export class CommandLineAutoApprover extends Disposable {
} }
// Check session allow list (session deny rules can't exist) // Check session allow list (session deny rules can't exist)
for (const rule of this._getSessionRules().allowListCommandLineRules) { for (const rule of this._getSessionRules(chatSessionId).allowListCommandLineRules) {
if (rule.regex.test(commandLine)) { if (rule.regex.test(commandLine)) {
return { return {
result: 'approved', result: 'approved',
@@ -176,7 +176,7 @@ export class CommandLineAutoApprover extends Disposable {
}; };
} }
private _getSessionRules(): { private _getSessionRules(chatSessionId?: string): {
denyListRules: IAutoApproveRule[]; denyListRules: IAutoApproveRule[];
allowListRules: IAutoApproveRule[]; allowListRules: IAutoApproveRule[];
allowListCommandLineRules: IAutoApproveRule[]; allowListCommandLineRules: IAutoApproveRule[];
@@ -187,7 +187,11 @@ export class CommandLineAutoApprover extends Disposable {
const allowListCommandLineRules: IAutoApproveRule[] = []; const allowListCommandLineRules: IAutoApproveRule[] = [];
const denyListCommandLineRules: IAutoApproveRule[] = []; const denyListCommandLineRules: IAutoApproveRule[] = [];
const sessionRulesConfig = this._terminalChatService.getSessionAutoApproveRules(); if (!chatSessionId) {
return { denyListRules, allowListRules, allowListCommandLineRules, denyListCommandLineRules };
}
const sessionRulesConfig = this._terminalChatService.getSessionAutoApproveRules(chatSessionId);
for (const [key, value] of Object.entries(sessionRulesConfig)) { for (const [key, value] of Object.entries(sessionRulesConfig)) {
if (typeof value === 'boolean') { if (typeof value === 'boolean') {
const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key);

View File

@@ -87,8 +87,8 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma
}; };
} }
const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os)); const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os, options.chatSessionId));
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine); const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine, options.chatSessionId);
const autoApproveReasons: string[] = [ const autoApproveReasons: string[] = [
...subCommandResults.map(e => e.reason), ...subCommandResults.map(e => e.reason),
commandLineResult.reason, commandLineResult.reason,