fix(security): enforce lead-only RBAC on team task create/cancel, broadcast, and auto-create

Members could create/cancel tasks, broadcast to team, and auto-create
tasks via spawn — all without role checks. System relied entirely on
LLM prompt behavior. Now enforced at system level via requireLead().
This commit is contained in:
viettranx committed 2026-03-09 10:00:41 +07:00
1 parent 3c03d1f482
commit 86ba785fe3
4 files changed
+28 -2

No files matched your search

+4
View File
@@ -53,7 +53,11 @@ func (dm *DelegateManager) prepareDelegation(ctx context.Context, opts DelegateO
// Auto-create team task when team_task_id is omitted.
// This eliminates the two-step create→spawn dance that caused LLM hallucination
// (LLM would call create+spawn in parallel, hallucinating the task_id).
// Only the team lead can create tasks — members must ask the lead.
if team != nil && opts.TeamTaskID == uuid.Nil {
if sourceAgentID != team.LeadAgentID {
return nil, nil, fmt.Errorf("only the team lead can create team tasks — ask your lead to assign this task")
}
subject := opts.Label
if subject == "" {
subject = opts.Task
+3
View File
@@ -139,6 +139,9 @@ func (t *TeamMessageTool) executeBroadcast(ctx context.Context, args map[string]
if err != nil {
return ErrorResult(err.Error())
}
if err := t.manager.requireLead(ctx, team, agentID); err != nil {
return ErrorResult(err.Error())
}
text, _ := args["text"].(string)
if text == "" {
+8 -2
View File
@@ -226,10 +226,13 @@ func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]inter
}
func (t *TeamTasksTool) executeCreate(ctx context.Context, args map[string]interface{}) *Result {
team, _, err := t.manager.resolveTeam(ctx)
team, agentID, err := t.manager.resolveTeam(ctx)
if err != nil {
return ErrorResult(err.Error())
}
if err := t.manager.requireLead(ctx, team, agentID); err != nil {
return ErrorResult(err.Error())
}
subject, _ := args["subject"].(string)
if subject == "" {
@@ -378,10 +381,13 @@ func (t *TeamTasksTool) executeCancel(ctx context.Context, args map[string]inter
return ErrorResult("delegate agents cannot cancel team tasks directly")
}
team, _, err := t.manager.resolveTeam(ctx)
team, agentID, err := t.manager.resolveTeam(ctx)
if err != nil {
return ErrorResult(err.Error())
}
if err := t.manager.requireLead(ctx, team, agentID); err != nil {
return ErrorResult(err.Error())
}
taskIDStr, _ := args["task_id"].(string)
if taskIDStr == "" {
+13
View File
@@ -87,6 +87,19 @@ func (m *TeamToolManager) resolveTeam(ctx context.Context) (*store.TeamData, uui
return team, agentID, nil
}
// requireLead checks if the calling agent is the team lead.
// Delegate/system channels bypass this check (they act on behalf of the lead).
func (m *TeamToolManager) requireLead(ctx context.Context, team *store.TeamData, agentID uuid.UUID) error {
channel := ToolChannelFromCtx(ctx)
if channel == "delegate" || channel == "system" {
return nil
}
if agentID != team.LeadAgentID {
return fmt.Errorf("only the team lead can perform this action")
}
return nil
}
// InvalidateTeam clears all cached team data.
// Called when team membership, settings, or links change.
// Full clear is acceptable because team mutations are rare (admin-initiated).