feat: enforce handoff allowlist with default-open policy
Add AllowlistChecker interface and wire it into HandoffTool to control which agents can delegate to which. Default behavior is open (allow all) when no subagents config is present; enforces allow_agents list when configured via CanSpawnSubagent.
This commit is contained in:
parent
eb707ff724
commit
f73c153578
3 changed files with 44 additions and 4 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue