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))
|
agent.Tools.Register(multiagent.NewBlackboardTool(placeholderBoard, agentID))
|
||||||
|
|
||||||
// Handoff tool: delegate tasks to other agents
|
// 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
|
// List agents tool: discover available agents
|
||||||
agent.Tools.Register(multiagent.NewListAgentsTool(resolver))
|
agent.Tools.Register(multiagent.NewListAgentsTool(resolver))
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,18 @@ type AgentResolver interface {
|
||||||
ListAgents() []AgentInfo
|
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,
|
// AgentInfo is a minimal view of an agent for handoff purposes,
|
||||||
// decoupled from the full AgentInstance to avoid circular imports.
|
// decoupled from the full AgentInstance to avoid circular imports.
|
||||||
type AgentInfo struct {
|
type AgentInfo struct {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@ type HandoffTool struct {
|
||||||
fromAgentID string
|
fromAgentID string
|
||||||
originChannel string
|
originChannel string
|
||||||
originChatID string
|
originChatID string
|
||||||
depth int // current handoff depth (0 = top-level)
|
depth int // current handoff depth (0 = top-level)
|
||||||
visited []string // agent IDs already in the call chain
|
visited []string // agent IDs already in the call chain
|
||||||
maxDepth int // max allowed depth (0 = use DefaultMaxHandoffDepth)
|
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.
|
// NewHandoffTool creates a handoff tool bound to a specific source agent.
|
||||||
|
|
@ -94,6 +95,12 @@ func (t *HandoffTool) SetBoard(board *Blackboard) {
|
||||||
t.board = board
|
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.
|
// SetContext updates the origin channel and chat ID for handoff routing.
|
||||||
func (t *HandoffTool) SetContext(channel, chatID string) {
|
func (t *HandoffTool) SetContext(channel, chatID string) {
|
||||||
t.originChannel = channel
|
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")
|
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
|
// Parse optional context map
|
||||||
var contextMap map[string]string
|
var contextMap map[string]string
|
||||||
if ctxRaw, ok := args["context"].(map[string]any); ok {
|
if ctxRaw, ok := args["context"].(map[string]any); ok {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue