feat: add allowlist denylist and preset list for tools

This commit is contained in:
MrAvalonApple 2026-04-07 19:39:50 +03:00
parent 48fbec6659
commit 62f285a558
3 changed files with 73 additions and 9 deletions

View File

@ -146,7 +146,9 @@ export class Agent {
maxTurns: this.config.maxTurns, maxTurns: this.config.maxTurns,
maxTokens: this.config.maxTokens, maxTokens: this.config.maxTokens,
temperature: this.config.temperature, temperature: this.config.temperature,
toolPreset: this.config.toolPreset,
allowedTools: this.config.tools, allowedTools: this.config.tools,
disallowedTools: this.config.disallowedTools,
agentName: this.name, agentName: this.name,
agentRole: this.config.systemPrompt?.slice(0, 50) ?? 'assistant', agentRole: this.config.systemPrompt?.slice(0, 50) ?? 'assistant',
loopDetection: this.config.loopDetection, loopDetection: this.config.loopDetection,

View File

@ -28,6 +28,7 @@ import type {
TraceEvent, TraceEvent,
LoopDetectionConfig, LoopDetectionConfig,
LoopDetectionInfo, LoopDetectionInfo,
LLMToolDef,
} from '../types.js' } from '../types.js'
import { TokenBudgetExceededError } from '../errors.js' import { TokenBudgetExceededError } from '../errors.js'
import { LoopDetector } from './loop-detector.js' import { LoopDetector } from './loop-detector.js'
@ -35,6 +36,17 @@ import { emitTrace } from '../utils/trace.js'
import type { ToolRegistry } from '../tool/framework.js' import type { ToolRegistry } from '../tool/framework.js'
import type { ToolExecutor } from '../tool/executor.js' import type { ToolExecutor } from '../tool/executor.js'
// ---------------------------------------------------------------------------
// Tool presets
// ---------------------------------------------------------------------------
/** Predefined tool sets for common agent use cases. */
export const TOOL_PRESETS = {
readonly: ['file_read', 'grep'],
readwrite: ['file_read', 'file_write', 'file_edit', 'grep'],
full: ['file_read', 'file_write', 'file_edit', 'grep', 'bash'],
} as const satisfies Record<string, readonly string[]>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Public interfaces // Public interfaces
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -60,11 +72,15 @@ export interface RunnerOptions {
/** AbortSignal that cancels any in-flight adapter call and stops the loop. */ /** AbortSignal that cancels any in-flight adapter call and stops the loop. */
readonly abortSignal?: AbortSignal readonly abortSignal?: AbortSignal
/** /**
* Whitelist of tool names this runner is allowed to use. * Tool access control configuration.
* When provided, only tools whose name appears in this list are sent to the * - `toolPreset`: Predefined tool sets for common use cases
* LLM. When omitted, all registered tools are available. * - `allowedTools`: Whitelist of tool names (allowlist)
* - `disallowedTools`: Blacklist of tool names (denylist)
* Tools are resolved in order: preset allowlist denylist
*/ */
readonly toolPreset?: 'readonly' | 'readwrite' | 'full'
readonly allowedTools?: readonly string[] readonly allowedTools?: readonly string[]
readonly disallowedTools?: readonly string[]
/** Display name of the agent driving this runner (used in tool context). */ /** Display name of the agent driving this runner (used in tool context). */
readonly agentName?: string readonly agentName?: string
/** Short role description of the agent (used in tool context). */ /** Short role description of the agent (used in tool context). */
@ -180,6 +196,52 @@ export class AgentRunner {
this.maxTurns = options.maxTurns ?? 10 this.maxTurns = options.maxTurns ?? 10
} }
// -------------------------------------------------------------------------
// Tool resolution
// -------------------------------------------------------------------------
/**
* Resolve the final set of tools available to this agent based on the
* three-layer configuration: preset allowlist denylist.
*
* Returns LLMToolDef[] for direct use with LLM adapters.
*/
private resolveTools(): LLMToolDef[] {
// Validate configuration for contradictions
if (this.options.allowedTools && this.options.disallowedTools) {
const overlap = this.options.allowedTools.filter(tool =>
this.options.disallowedTools!.includes(tool)
)
if (overlap.length > 0) {
console.warn(
`AgentRunner: tool "${overlap[0]}" appears in both allowedTools and disallowedTools. ` +
'This is contradictory and may lead to unexpected behavior.'
)
}
}
let tools = this.toolRegistry.toToolDefs()
// 1. Apply preset filter if set
if (this.options.toolPreset) {
const presetTools = new Set(TOOL_PRESETS[this.options.toolPreset] as readonly string[])
tools = tools.filter(t => presetTools.has(t.name))
}
// 2. Apply allowlist filter if set
if (this.options.allowedTools) {
tools = tools.filter(t => this.options.allowedTools!.includes(t.name))
}
// 3. Apply denylist filter if set
if (this.options.disallowedTools) {
const denied = new Set(this.options.disallowedTools)
tools = tools.filter(t => !denied.has(t.name))
}
return tools
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Public API // Public API
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -241,12 +303,8 @@ export class AgentRunner {
let budgetExceeded = false let budgetExceeded = false
// Build the stable LLM options once; model / tokens / temp don't change. // Build the stable LLM options once; model / tokens / temp don't change.
// toToolDefs() returns LLMToolDef[] (inputSchema, camelCase) — matches // resolveTools() returns LLMToolDef[] with three-layer filtering applied.
// LLMChatOptions.tools from types.ts directly. const toolDefs = this.resolveTools()
const allDefs = this.toolRegistry.toToolDefs()
const toolDefs = this.options.allowedTools
? allDefs.filter(d => this.options.allowedTools!.includes(d.name))
: allDefs
// Per-call abortSignal takes precedence over the static one. // Per-call abortSignal takes precedence over the static one.
const effectiveAbortSignal = options.abortSignal ?? this.options.abortSignal const effectiveAbortSignal = options.abortSignal ?? this.options.abortSignal

View File

@ -207,6 +207,10 @@ export interface AgentConfig {
readonly systemPrompt?: string readonly systemPrompt?: string
/** Names of tools (from the tool registry) available to this agent. */ /** Names of tools (from the tool registry) available to this agent. */
readonly tools?: readonly string[] readonly tools?: readonly string[]
/** Names of tools explicitly disallowed for this agent. */
readonly disallowedTools?: readonly string[]
/** Predefined tool preset for common use cases. */
readonly toolPreset?: 'readonly' | 'readwrite' | 'full'
readonly maxTurns?: number readonly maxTurns?: number
readonly maxTokens?: number readonly maxTokens?: number
/** Maximum cumulative tokens (input + output) allowed for this run. */ /** Maximum cumulative tokens (input + output) allowed for this run. */