candidate: Restrict set of tools when agent mode setting is disabled (#282871)

* Restrict set of tools when agent mode setting is disabled (#282623)

* restrict set of tools when agent mode setting is disabled (https://github.com/microsoft/vscode-internalbacklog/issues/6432)

* include read/search/web

* tweaks to #282623 (#282889)

* update chat.agent.enabled description

* code suggestions

* includeDisabled bug
This commit is contained in:
Josh Spicer 2025-12-11 15:37:19 -08:00 committed by GitHub
parent a1e5f6c336
commit b791a56d01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 38 additions and 8 deletions

View File

@ -163,7 +163,7 @@
"localization": {
"description": {
"key": "chat.agent.enabled.description",
"value": "Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view."
"value": "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used."
}
},
"type": "boolean",

View File

@ -538,7 +538,7 @@ configurationRegistry.registerConfiguration({
},
[ChatConfiguration.AgentEnabled]: {
type: 'boolean',
description: nls.localize('chat.agent.enabled.description', "Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view."),
description: nls.localize('chat.agent.enabled.description', "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used."),
default: true,
policy: {
name: 'ChatAgentMode',
@ -548,7 +548,7 @@ configurationRegistry.registerConfiguration({
localization: {
description: {
key: 'chat.agent.enabled.description',
value: nls.localize('chat.agent.enabled.description', "Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view."),
value: nls.localize('chat.agent.enabled.description', "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used."),
}
}
}

View File

@ -16,7 +16,7 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { derived, IObservable, observableFromEventOpts, ObservableSet } from '../../../../base/common/observable.js';
import { derived, IObservable, IReader, observableFromEventOpts, ObservableSet } from '../../../../base/common/observable.js';
import Severity from '../../../../base/common/severity.js';
import { StopWatch } from '../../../../base/common/stopwatch.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
@ -29,6 +29,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
@ -93,6 +94,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
private readonly _callsByRequestId = new Map<string, ITrackedCall[]>();
private readonly _isAgentModeEnabled: IObservable<boolean>;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IExtensionService private readonly _extensionService: IExtensionService,
@ -109,6 +112,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
) {
super();
this._isAgentModeEnabled = observableConfigValue(ChatConfiguration.AgentEnabled, true, this._configurationService);
this._register(this._contextKeyService.onDidChangeContext(e => {
if (e.affectsSome(this._toolContextKeys)) {
// Not worth it to compute a delta here unless we have many tools changing often
@ -117,7 +122,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
}));
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled)) {
if (e.affectsConfiguration(ChatConfiguration.ExtensionToolsEnabled) || e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {
this._onDidChangeToolsScheduler.schedule();
}
}));
@ -166,6 +171,27 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
}
));
}
/**
* Returns if the given tool or toolset is permitted in the current context.
* When agent mode is enabled, all tools are permitted (no restriction)
* When agent mode is disabled only a subset of read-only tools are permitted in agentic-loop contexts.
*/
private isPermitted(toolOrToolSet: IToolData | ToolSet, reader?: IReader): boolean {
const agentModeEnabled = this._isAgentModeEnabled.read(reader);
if (agentModeEnabled !== false) {
return true;
}
const permittedInternalToolSetIds = [SpecedToolAliases.read, SpecedToolAliases.search, SpecedToolAliases.web];
if (toolOrToolSet instanceof ToolSet) {
const permitted = toolOrToolSet.source.type === 'internal' && permittedInternalToolSetIds.includes(toolOrToolSet.referenceName);
this._logService.trace(`LanguageModelToolsService#isPermitted: ToolSet ${toolOrToolSet.id} (${toolOrToolSet.referenceName}) permitted=${permitted}`);
return permitted;
}
this._logService.trace(`LanguageModelToolsService#isPermitted: Tool ${toolOrToolSet.id} (${toolOrToolSet.toolReferenceName}) permitted=false`);
return false;
}
override dispose(): void {
super.dispose();
@ -243,7 +269,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
toolData => {
const satisfiesWhenClause = includeDisabled || !toolData.when || this._contextKeyService.contextMatchesRules(toolData.when);
const satisfiesExternalToolCheck = toolData.source.type !== 'extension' || !!extensionToolsEnabled;
return satisfiesWhenClause && satisfiesExternalToolCheck;
const satisfiesPermittedCheck = includeDisabled || this.isPermitted(toolData);
return satisfiesWhenClause && satisfiesExternalToolCheck && satisfiesPermittedCheck;
});
}
@ -853,7 +880,10 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
private readonly _toolSets = new ObservableSet<ToolSet>();
readonly toolSets: IObservable<Iterable<ToolSet>> = this._toolSets.observable;
readonly toolSets: IObservable<Iterable<ToolSet>> = derived(this, reader => {
const allToolSets = Array.from(this._toolSets.observable.read(reader));
return allToolSets.filter(toolSet => this.isPermitted(toolSet, reader));
});
getToolSet(id: string): ToolSet | undefined {
for (const toolSet of this._toolSets) {
@ -916,7 +946,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
}
}
for (const tool of this.toolsObservable.read(reader)) {
if (tool.canBeReferencedInPrompt && !coveredByToolSets.has(tool)) {
if (tool.canBeReferencedInPrompt && !coveredByToolSets.has(tool) && this.isPermitted(tool, reader)) {
result.push([tool, getToolFullReferenceName(tool)]);
}
}