feat(tools): integrate Profile tools config with runner

Add mergeToolsConfig function to combine Profile tools config with
CLI options. Profile config serves as base, CLI options override.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jiang Bohan 2026-01-31 01:57:51 +08:00
parent 9467ac9fff
commit 3c0f132ff8
2 changed files with 104 additions and 12 deletions

View file

@ -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) {

View file

@ -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<string, ToolPolicy> = {};
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;
}