diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index cc31e06a8..9315ccf34 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -181,7 +181,23 @@ func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *A agent.Tools.Register(multiagent.NewBlackboardTool(placeholderBoard, agentID)) // Handoff tool: delegate tasks to other agents - agent.Tools.Register(multiagent.NewHandoffTool(resolver, placeholderBoard, agentID)) + handoffTool := multiagent.NewHandoffTool(resolver, placeholderBoard, agentID) + + // Allowlist checker: default-open when no subagents config, + // enforces allow_agents when configured. + currentAgentIDForHandoff := agentID + handoffTool.SetAllowlistChecker(multiagent.AllowlistCheckerFunc(func(from, to string) bool { + parent, ok := registry.GetAgent(from) + if !ok { + return false + } + // Default open: if no allowlist configured, allow all handoffs + if parent.Subagents == nil || parent.Subagents.AllowAgents == nil { + return true + } + return registry.CanSpawnSubagent(currentAgentIDForHandoff, to) + })) + agent.Tools.Register(handoffTool) // List agents tool: discover available agents agent.Tools.Register(multiagent.NewListAgentsTool(resolver)) diff --git a/pkg/multiagent/handoff.go b/pkg/multiagent/handoff.go index 50d342885..fe8063f1f 100644 --- a/pkg/multiagent/handoff.go +++ b/pkg/multiagent/handoff.go @@ -16,6 +16,18 @@ type AgentResolver interface { ListAgents() []AgentInfo } +// AllowlistChecker determines whether a handoff from one agent to another is allowed. +type AllowlistChecker interface { + CanHandoff(fromAgentID, toAgentID string) bool +} + +// AllowlistCheckerFunc adapts a function to the AllowlistChecker interface. +type AllowlistCheckerFunc func(fromAgentID, toAgentID string) bool + +func (f AllowlistCheckerFunc) CanHandoff(fromAgentID, toAgentID string) bool { + return f(fromAgentID, toAgentID) +} + // AgentInfo is a minimal view of an agent for handoff purposes, // decoupled from the full AgentInstance to avoid circular imports. type AgentInfo struct { diff --git a/pkg/multiagent/handoff_tool.go b/pkg/multiagent/handoff_tool.go index e0d344d91..98e3cbd48 100644 --- a/pkg/multiagent/handoff_tool.go +++ b/pkg/multiagent/handoff_tool.go @@ -15,9 +15,10 @@ type HandoffTool struct { fromAgentID string originChannel string originChatID string - depth int // current handoff depth (0 = top-level) - visited []string // agent IDs already in the call chain - maxDepth int // max allowed depth (0 = use DefaultMaxHandoffDepth) + depth int // current handoff depth (0 = top-level) + visited []string // agent IDs already in the call chain + maxDepth int // max allowed depth (0 = use DefaultMaxHandoffDepth) + allowlistChecker AllowlistChecker // optional; nil = allow all } // NewHandoffTool creates a handoff tool bound to a specific source agent. @@ -94,6 +95,12 @@ func (t *HandoffTool) SetBoard(board *Blackboard) { t.board = board } +// SetAllowlistChecker sets an optional checker that controls which agents +// can be handed off to. If nil, all handoffs are allowed. +func (t *HandoffTool) SetAllowlistChecker(checker AllowlistChecker) { + t.allowlistChecker = checker +} + // SetContext updates the origin channel and chat ID for handoff routing. func (t *HandoffTool) SetContext(channel, chatID string) { t.originChannel = channel @@ -122,6 +129,11 @@ func (t *HandoffTool) Execute(ctx context.Context, args map[string]any) *tools.T return tools.ErrorResult("agent_id or capability is required") } + // Allowlist check: if a checker is set and it denies the handoff, block it. + if t.allowlistChecker != nil && !t.allowlistChecker.CanHandoff(t.fromAgentID, agentID) { + return tools.ErrorResult(fmt.Sprintf("handoff from %q to %q not allowed by policy", t.fromAgentID, agentID)) + } + // Parse optional context map var contextMap map[string]string if ctxRaw, ok := args["context"].(map[string]any); ok {