fix(coding-agent): use tool-name allowlists and remove cwd-bound singletons

- treat tools as a global allowlist across built-in, extension, and SDK tools
- remove process-cwd singleton tool usage from SDK and CLI paths
- add regression coverage for extension tool filtering

closes #3452
closes #2835
This commit is contained in:
Mario Zechner
2026-04-20 21:53:07 +02:00
parent ed89480f20
commit 27c1544839
16 changed files with 295 additions and 199 deletions

View File

@@ -150,6 +150,8 @@ export interface AgentSessionConfig {
modelRegistry: ModelRegistry;
/** Initial active built-in tool names. Default: [read, bash, edit, write] */
initialActiveToolNames?: string[];
/** Optional allowlist of tool names. When provided, only these tool names are exposed. */
allowedToolNames?: string[];
/**
* Override base tools (useful for custom runtimes).
*
@@ -278,6 +280,7 @@ export class AgentSession {
private _cwd: string;
private _extensionRunnerRef?: { current?: ExtensionRunner };
private _initialActiveToolNames?: string[];
private _allowedToolNames?: Set<string>;
private _baseToolsOverride?: Record<string, AgentTool>;
private _sessionStartEvent: SessionStartEvent;
private _extensionUIContext?: ExtensionUIContext;
@@ -309,6 +312,7 @@ export class AgentSession {
this._modelRegistry = config.modelRegistry;
this._extensionRunnerRef = config.extensionRunnerRef;
this._initialActiveToolNames = config.initialActiveToolNames;
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
this._baseToolsOverride = config.baseToolsOverride;
this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
@@ -2221,6 +2225,8 @@ export class AgentSession {
private _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void {
const previousRegistryNames = new Set(this._toolRegistry.keys());
const previousActiveToolNames = this.getActiveToolNames();
const allowedToolNames = this._allowedToolNames;
const isAllowedTool = (name: string): boolean => !allowedToolNames || allowedToolNames.has(name);
const registeredTools = this._extensionRunner.getAllRegisteredTools();
const allCustomTools = [
@@ -2229,15 +2235,17 @@ export class AgentSession {
definition,
sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
})),
];
].filter((tool) => isAllowedTool(tool.definition.name));
const definitionRegistry = new Map<string, ToolDefinitionEntry>(
Array.from(this._baseToolDefinitions.entries()).map(([name, definition]) => [
name,
{
definition,
sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
},
]),
Array.from(this._baseToolDefinitions.entries())
.filter(([name]) => isAllowedTool(name))
.map(([name, definition]) => [
name,
{
definition,
sourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: "builtin" }),
},
]),
);
for (const tool of allCustomTools) {
definitionRegistry.set(tool.definition.name, {
@@ -2265,10 +2273,12 @@ export class AgentSession {
const runner = this._extensionRunner;
const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);
const wrappedBuiltInTools = wrapRegisteredTools(
Array.from(this._baseToolDefinitions.values()).map((definition) => ({
definition,
sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, { source: "builtin" }),
})),
Array.from(this._baseToolDefinitions.values())
.filter((definition) => isAllowedTool(definition.name))
.map((definition) => ({
definition,
sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, { source: "builtin" }),
})),
runner,
);
@@ -2278,11 +2288,17 @@ export class AgentSession {
}
this._toolRegistry = toolRegistry;
const nextActiveToolNames = options?.activeToolNames
? [...options.activeToolNames]
: [...previousActiveToolNames];
const nextActiveToolNames = (
options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]
).filter((name) => isAllowedTool(name));
if (options?.includeAllExtensionTools) {
if (allowedToolNames) {
for (const toolName of this._toolRegistry.keys()) {
if (allowedToolNames.has(toolName)) {
nextActiveToolNames.push(toolName);
}
}
} else if (options?.includeAllExtensionTools) {
for (const tool of wrappedExtensionTools) {
nextActiveToolNames.push(tool.name);
}