diff --git a/src/agent/runner.ts b/src/agent/runner.ts index bcea33b3..5017b8cb 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -11,6 +11,7 @@ import { DEFAULT_CONTEXT_TOKENS, type ContextWindowGuardResult, } from "./context-window/index.js"; +import { mergeToolsConfig, type ToolsConfig } from "./tools/policy.js"; /** * Get API Key based on provider. @@ -249,7 +250,21 @@ export class Agent { } this.agent.setModel(model); - this.agent.setTools(resolveTools(options)); + + // Merge Profile tools config with options.tools (options takes precedence) + const profileToolsConfig = this.profile?.getToolsConfig(); + const mergedToolsConfig = mergeToolsConfig(profileToolsConfig, options.tools); + const toolsOptions = mergedToolsConfig ? { ...options, tools: mergedToolsConfig } : options; + + const tools = resolveTools(toolsOptions); + if (this.debug) { + if (profileToolsConfig) { + console.error(`[debug] Profile tools config: ${JSON.stringify(profileToolsConfig)}`); + } + console.error(`[debug] Merged tools config: ${JSON.stringify(mergedToolsConfig)}`); + console.error(`[debug] Resolved ${tools.length} tools: ${tools.map(t => t.name).join(", ") || "(none)"}`); + } + this.agent.setTools(tools); const restoredMessages = this.session.loadMessages(); if (restoredMessages.length > 0) { diff --git a/src/agent/tools/policy.ts b/src/agent/tools/policy.ts index 765cdd07..7e1a9407 100644 --- a/src/agent/tools/policy.ts +++ b/src/agent/tools/policy.ts @@ -170,9 +170,11 @@ function resolveProviderPolicy( * Get subagent tool policy. */ export function getSubagentPolicy(extraDeny?: string[]): ToolPolicy { - return { - deny: mergeDeny(DEFAULT_SUBAGENT_TOOL_DENY, extraDeny), - }; + const deny = mergeDeny(DEFAULT_SUBAGENT_TOOL_DENY, extraDeny); + if (deny) { + return { deny }; + } + return {}; } // ============================================================================ @@ -214,10 +216,13 @@ export function filterTools( // Layer 2: Global allow/deny if (config?.allow || config?.deny) { - const globalPolicy: ToolPolicy = { - allow: config.allow, - deny: config.deny, - }; + const globalPolicy: ToolPolicy = {}; + if (config.allow) { + globalPolicy.allow = config.allow; + } + if (config.deny) { + globalPolicy.deny = config.deny; + } filtered = filterToolsByPolicy(filtered, globalPolicy); } @@ -238,6 +243,75 @@ export function filterTools( return filtered; } +/** + * Merge two ToolsConfig objects. + * The override config takes precedence: + * - profile: override wins if set + * - allow: union of both + * - deny: union of both + * - byProvider: deep merge with override taking precedence + */ +export function mergeToolsConfig( + base?: ToolsConfig, + override?: ToolsConfig, +): ToolsConfig | undefined { + if (!base && !override) return undefined; + if (!base) return override; + if (!override) return base; + + const result: ToolsConfig = {}; + + // profile: override wins + const profile = override.profile ?? base.profile; + if (profile) { + result.profile = profile; + } + + // allow: union + const allow = mergeAllow(base.allow, override.allow); + if (allow) { + result.allow = allow; + } + + // deny: union + const deny = mergeDeny(base.deny, override.deny); + if (deny) { + result.deny = deny; + } + + // byProvider: deep merge + if (base.byProvider || override.byProvider) { + const providers = new Set([ + ...Object.keys(base.byProvider ?? {}), + ...Object.keys(override.byProvider ?? {}), + ]); + + const byProvider: Record = {}; + for (const provider of providers) { + const basePolicy = base.byProvider?.[provider]; + const overridePolicy = override.byProvider?.[provider]; + + if (basePolicy && overridePolicy) { + const merged: ToolPolicy = {}; + const pAllow = mergeAllow(basePolicy.allow, overridePolicy.allow); + if (pAllow) { + merged.allow = pAllow; + } + const pDeny = mergeDeny(basePolicy.deny, overridePolicy.deny); + if (pDeny) { + merged.deny = pDeny; + } + byProvider[provider] = merged; + } else { + byProvider[provider] = overridePolicy ?? basePolicy!; + } + } + result.byProvider = byProvider; + } + + return Object.keys(result).length > 0 ? result : undefined; +} + /** * Check if a specific tool would be allowed given the options. */ @@ -257,10 +331,13 @@ export function wouldToolBeAllowed( // Layer 2: Global allow/deny if (config?.allow || config?.deny) { - const globalPolicy: ToolPolicy = { - allow: config.allow, - deny: config.deny, - }; + const globalPolicy: ToolPolicy = {}; + if (config.allow) { + globalPolicy.allow = config.allow; + } + if (config.deny) { + globalPolicy.deny = config.deny; + } if (!isToolAllowed(toolName, globalPolicy)) { return false; }