@@ -28,13 +28,30 @@ import type {
2828 TraceEvent ,
2929 LoopDetectionConfig ,
3030 LoopDetectionInfo ,
31+ LLMToolDef ,
3132} from '../types.js'
3233import { TokenBudgetExceededError } from '../errors.js'
3334import { LoopDetector } from './loop-detector.js'
3435import { emitTrace } from '../utils/trace.js'
3536import type { ToolRegistry } from '../tool/framework.js'
3637import type { ToolExecutor } from '../tool/executor.js'
3738
39+ // ---------------------------------------------------------------------------
40+ // Tool presets
41+ // ---------------------------------------------------------------------------
42+
43+ /** Predefined tool sets for common agent use cases. */
44+ export const TOOL_PRESETS = {
45+ readonly : [ 'file_read' , 'grep' , 'glob' ] ,
46+ readwrite : [ 'file_read' , 'file_write' , 'file_edit' , 'grep' , 'glob' ] ,
47+ full : [ 'file_read' , 'file_write' , 'file_edit' , 'grep' , 'glob' , 'bash' ] ,
48+ } as const satisfies Record < string , readonly string [ ] >
49+
50+ /** Framework-level disallowed tools for safety rails. */
51+ export const AGENT_FRAMEWORK_DISALLOWED : readonly string [ ] = [
52+ // Empty for now, infrastructure for future built-in tools
53+ ]
54+
3855// ---------------------------------------------------------------------------
3956// Public interfaces
4057// ---------------------------------------------------------------------------
@@ -60,11 +77,15 @@ export interface RunnerOptions {
6077 /** AbortSignal that cancels any in-flight adapter call and stops the loop. */
6178 readonly abortSignal ?: AbortSignal
6279 /**
63- * Whitelist of tool names this runner is allowed to use.
64- * When provided, only tools whose name appears in this list are sent to the
65- * LLM. When omitted, all registered tools are available.
80+ * Tool access control configuration.
81+ * - `toolPreset`: Predefined tool sets for common use cases
82+ * - `allowedTools`: Whitelist of tool names (allowlist)
83+ * - `disallowedTools`: Blacklist of tool names (denylist)
84+ * Tools are resolved in order: preset → allowlist → denylist
6685 */
86+ readonly toolPreset ?: 'readonly' | 'readwrite' | 'full'
6787 readonly allowedTools ?: readonly string [ ]
88+ readonly disallowedTools ?: readonly string [ ]
6889 /** Display name of the agent driving this runner (used in tool context). */
6990 readonly agentName ?: string
7091 /** Short role description of the agent (used in tool context). */
@@ -180,6 +201,67 @@ export class AgentRunner {
180201 this . maxTurns = options . maxTurns ?? 10
181202 }
182203
204+ // -------------------------------------------------------------------------
205+ // Tool resolution
206+ // -------------------------------------------------------------------------
207+
208+ /**
209+ * Resolve the final set of tools available to this agent based on the
210+ * three-layer configuration: preset → allowlist → denylist → framework safety.
211+ *
212+ * Returns LLMToolDef[] for direct use with LLM adapters.
213+ */
214+ private resolveTools ( ) : LLMToolDef [ ] {
215+ // Validate configuration for contradictions
216+ if ( this . options . toolPreset && this . options . allowedTools ) {
217+ console . warn (
218+ 'AgentRunner: both toolPreset and allowedTools are set. ' +
219+ 'Final tool access will be the intersection of both.'
220+ )
221+ }
222+
223+ if ( this . options . allowedTools && this . options . disallowedTools ) {
224+ const overlap = this . options . allowedTools . filter ( tool =>
225+ this . options . disallowedTools ! . includes ( tool )
226+ )
227+ if ( overlap . length > 0 ) {
228+ console . warn (
229+ `AgentRunner: tools [${ overlap . map ( name => `"${ name } "` ) . join ( ', ' ) } ] appear in both allowedTools and disallowedTools. ` +
230+ 'This is contradictory and may lead to unexpected behavior.'
231+ )
232+ }
233+ }
234+
235+ const allTools = this . toolRegistry . toToolDefs ( )
236+ const runtimeCustomTools = this . toolRegistry . toRuntimeToolDefs ( )
237+ const runtimeCustomToolNames = new Set ( runtimeCustomTools . map ( t => t . name ) )
238+ let filteredTools = allTools . filter ( t => ! runtimeCustomToolNames . has ( t . name ) )
239+
240+ // 1. Apply preset filter if set
241+ if ( this . options . toolPreset ) {
242+ const presetTools = new Set ( TOOL_PRESETS [ this . options . toolPreset ] as readonly string [ ] )
243+ filteredTools = filteredTools . filter ( t => presetTools . has ( t . name ) )
244+ }
245+
246+ // 2. Apply allowlist filter if set
247+ if ( this . options . allowedTools ) {
248+ filteredTools = filteredTools . filter ( t => this . options . allowedTools ! . includes ( t . name ) )
249+ }
250+
251+ // 3. Apply denylist filter if set
252+ if ( this . options . disallowedTools ) {
253+ const denied = new Set ( this . options . disallowedTools )
254+ filteredTools = filteredTools . filter ( t => ! denied . has ( t . name ) )
255+ }
256+
257+ // 4. Apply framework-level safety rails
258+ const frameworkDenied = new Set ( AGENT_FRAMEWORK_DISALLOWED )
259+ filteredTools = filteredTools . filter ( t => ! frameworkDenied . has ( t . name ) )
260+
261+ // Runtime-added custom tools stay available regardless of filtering rules.
262+ return [ ...filteredTools , ...runtimeCustomTools ]
263+ }
264+
183265 // -------------------------------------------------------------------------
184266 // Public API
185267 // -------------------------------------------------------------------------
@@ -241,12 +323,8 @@ export class AgentRunner {
241323 let budgetExceeded = false
242324
243325 // Build the stable LLM options once; model / tokens / temp don't change.
244- // toToolDefs() returns LLMToolDef[] (inputSchema, camelCase) — matches
245- // LLMChatOptions.tools from types.ts directly.
246- const allDefs = this . toolRegistry . toToolDefs ( )
247- const toolDefs = this . options . allowedTools
248- ? allDefs . filter ( d => this . options . allowedTools ! . includes ( d . name ) )
249- : allDefs
326+ // resolveTools() returns LLMToolDef[] with three-layer filtering applied.
327+ const toolDefs = this . resolveTools ( )
250328
251329 // Per-call abortSignal takes precedence over the static one.
252330 const effectiveAbortSignal = options . abortSignal ?? this . options . abortSignal
0 commit comments