mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 22:17:36 +00:00
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:
1 parent
3c03d1f482
commit
86ba785fe3
4 files changed
+28
-2
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user