diff --git a/core/agent/actions.go b/core/agent/actions.go index 5ffb1ec1..da92ffdd 100644 --- a/core/agent/actions.go +++ b/core/agent/actions.go @@ -138,6 +138,37 @@ func (a *Agent) availableActions(j *types.Job) types.Actions { return defaultActions } +// filterActions constrains a set of actions to a per-agent allow/deny-list by tool name. +// If allow is non-empty, only actions whose name is in allow survive; names in deny are +// always removed. Empty allow and deny → input is returned unchanged (backward compatible). +func filterActions(acts types.Actions, allow, deny []string) types.Actions { + if len(allow) == 0 && len(deny) == 0 { + return acts + } + allowSet := make(map[string]struct{}, len(allow)) + for _, n := range allow { + allowSet[n] = struct{}{} + } + denySet := make(map[string]struct{}, len(deny)) + for _, n := range deny { + denySet[n] = struct{}{} + } + out := make(types.Actions, 0, len(acts)) + for _, act := range acts { + name := string(act.Definition().Name) + if len(allowSet) > 0 { + if _, ok := allowSet[name]; !ok { + continue + } + } + if _, ok := denySet[name]; ok { + continue + } + out = append(out, act) + } + return out +} + func (a *Agent) prepareHUD() (promptHUD *PromptHUD) { if !a.options.enableHUD { return nil diff --git a/core/agent/agent.go b/core/agent/agent.go index aacb5d60..b420b085 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -858,8 +858,13 @@ func (a *Agent) consumeJob(job *types.Job, role string) { } availableActions := a.getAvailableActionsForJob(job) + // Per-agent tool allow/deny-list: constrain the EFFECTIVE tool set across all + // sources (built-in, KB-injected and MCP) at this single assembly point, so both + // the tools presented to the model (cogitoTools) and the lookup set (allActions) + // respect it. Empty allow/deny = unchanged behaviour. + availableActions = filterActions(availableActions, a.options.allowedTools, a.options.excludedTools) cogitoTools := availableActions.ToCogitoTools(job.GetContext(), a.sharedState) - allActions := append(availableActions, a.mcpActionDefinitions...) + allActions := append(availableActions, filterActions(a.mcpActionDefinitions, a.options.allowedTools, a.options.excludedTools)...) obs := job.Obs if obs == nil && a.observer != nil && job.Obs != nil { diff --git a/core/agent/options.go b/core/agent/options.go index 7f489a32..338e61dc 100644 --- a/core/agent/options.go +++ b/core/agent/options.go @@ -40,6 +40,7 @@ type options struct { randomIdentity bool userActions types.Actions jobFilters types.JobFilters + allowedTools, excludedTools []string enableHUD, standaloneJob, showCharacter, enableKB, enableSummaryMemory, enableLongTermMemory bool stripThinkingTags bool kbAutoSearch bool @@ -512,6 +513,17 @@ func WithKBAutoSearch(enabled bool) Option { } } +// WithToolFilter constrains the agent's effective tool set. If allow is non-empty, +// only those tool names survive; deny names are always removed. Applied across ALL +// tool sources (MCP, built-in, KB-injected) at the single assembly point. +func WithToolFilter(allow, deny []string) Option { + return func(o *options) error { + o.allowedTools = allow + o.excludedTools = deny + return nil + } +} + // WithSchedulerStorePath sets the path for the scheduler's JSON storage file func WithSchedulerStorePath(path string) Option { return func(o *options) error { diff --git a/core/state/config.go b/core/state/config.go index 9abd3afe..ebcd7a93 100644 --- a/core/state/config.go +++ b/core/state/config.go @@ -105,6 +105,11 @@ type AgentConfig struct { StripThinkingTags bool `json:"strip_thinking_tags" form:"strip_thinking_tags"` EnableEvaluation bool `json:"enable_evaluation" form:"enable_evaluation"` MaxEvaluationLoops int `json:"max_evaluation_loops" form:"max_evaluation_loops"` + // AllowedTools/ExcludedTools constrain the agent's EFFECTIVE tool set (across MCP, + // built-in and KB-injected actions). If AllowedTools is non-empty, only those tool + // names are kept; ExcludedTools are always removed. Empty = unchanged behaviour. + AllowedTools []string `json:"allowed_tools" form:"allowed_tools"` + ExcludedTools []string `json:"excluded_tools" form:"excluded_tools"` } type AgentConfigMeta struct { diff --git a/core/state/pool.go b/core/state/pool.go index 66a754de..bd2cfdf1 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -520,6 +520,10 @@ func (a *AgentPool) startAgentWithConfig(name, pooldir string, config *AgentConf opts = append(opts, EnableGuidedTools) } + if len(config.AllowedTools) > 0 || len(config.ExcludedTools) > 0 { + opts = append(opts, WithToolFilter(config.AllowedTools, config.ExcludedTools)) + } + if config.StripThinkingTags { opts = append(opts, EnableStripThinkingTags) }