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');
// Handle session-scoped rules (temporary, in-memory only)
const chatSessionId = this.context.element.sessionId;
for (const rule of sessionRules) {
this.terminalChatService.addSessionAutoApproveRule(rule.key, rule.value);
this.terminalChatService.addSessionAutoApproveRule(chatSessionId, rule.key, rule.value);
}
// Handle workspace-scoped rules

View File

@@ -220,16 +220,18 @@ export interface ITerminalChatService {
/**
* 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 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.
* @returns A record of all session-scoped auto-approve rules
* Get all session-scoped auto-approve rules for a specific chat session.
* @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>();
/**
* Tracks session-scoped auto-approve rules. These are temporary rules that last only for the
* duration of the VS Code session (not persisted to disk).
* Tracks session-scoped auto-approve rules per chat session. These are temporary rules that
* 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(
@ILogService private readonly _logService: ILogService,
@@ -72,6 +73,16 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ
this._hasHiddenToolTerminalContext = TerminalChatContextKeys.hasHiddenChatTerminals.bindTo(this._contextKeyService);
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 {
@@ -320,11 +331,16 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ
return this._sessionAutoApprovalEnabled.has(chatSessionId);
}
addSessionAutoApproveRule(key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void {
this._sessionAutoApproveRules[key] = value;
addSessionAutoApproveRule(chatSessionId: string, key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void {
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 }>> {
return this._sessionAutoApproveRules;
getSessionAutoApproveRules(chatSessionId: string): Readonly<Record<string, boolean | { approve: boolean; matchCommandLine?: boolean }>> {
return this._sessionAutoApproveRules.get(chatSessionId) ?? {};
}
}

View File

@@ -85,7 +85,7 @@ export class CommandLineAutoApprover extends Disposable {
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
// always deny for now as it can easily lead to execute other commands
if (transientEnvVarRegex.test(command)) {
@@ -107,7 +107,7 @@ export class CommandLineAutoApprover extends Disposable {
}
// 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)) {
return {
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
for (const rule of this._denyListCommandLineRules) {
if (rule.regex.test(commandLine)) {
@@ -150,7 +150,7 @@ export class CommandLineAutoApprover extends Disposable {
}
// 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)) {
return {
result: 'approved',
@@ -176,7 +176,7 @@ export class CommandLineAutoApprover extends Disposable {
};
}
private _getSessionRules(): {
private _getSessionRules(chatSessionId?: string): {
denyListRules: IAutoApproveRule[];
allowListRules: IAutoApproveRule[];
allowListCommandLineRules: IAutoApproveRule[];
@@ -187,7 +187,11 @@ export class CommandLineAutoApprover extends Disposable {
const allowListCommandLineRules: 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)) {
if (typeof value === 'boolean') {
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 commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine);
const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os, options.chatSessionId));
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine, options.chatSessionId);
const autoApproveReasons: string[] = [
...subCommandResults.map(e => e.reason),
commandLineResult.reason,