fix(agent): revise agent permission model for visibility and mentions

- ListAgents: private agents are now visible to all workspace members
  (previously hidden from non-owner members)
- Mentions: private agents can only be @mentioned by the agent owner or
  workspace admin/owner; regular members' mentions of private agents are
  silently ignored
- Settings (update/delete/skills) and assign were already correctly
  restricted in previous PRs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yushen 2026-04-02 12:45:31 +08:00
parent f05f3face3
commit 68da1efd74
2 changed files with 24 additions and 16 deletions

View file

@ -142,8 +142,7 @@ func taskToResponse(t db.AgentTaskQueue) AgentTaskResponse {
func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
workspaceID := resolveWorkspaceID(r)
member, ok := h.workspaceMember(w, r, workspaceID)
if !ok {
if _, ok := h.workspaceMember(w, r, workspaceID); !ok {
return
}
@ -153,9 +152,6 @@ func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
return
}
userID := requestUserID(r)
isAdmin := roleAllowed(member.Role, "owner", "admin")
// Batch-load skills for all agents to avoid N+1.
skillRows, err := h.Queries.ListAgentSkillsByWorkspace(r.Context(), parseUUID(workspaceID))
if err != nil {
@ -172,21 +168,15 @@ func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
})
}
// Filter private agents: only visible to owner_id or workspace admin
var visible []AgentResponse
// All agents (including private) are visible to workspace members.
visible := make([]AgentResponse, 0, len(agents))
for _, a := range agents {
if a.Visibility == "private" && !isAdmin && uuidToString(a.OwnerID) != userID {
continue
}
resp := agentToResponse(a)
if skills, ok := skillMap[resp.ID]; ok {
resp.Skills = skills
}
visible = append(visible, resp)
}
if visible == nil {
visible = []AgentResponse{}
}
writeJSON(w, http.StatusOK, visible)
}

View file

@ -245,11 +245,14 @@ func (h *Handler) isReplyToMemberThread(parent *db.Comment, content string, issu
// enqueueMentionedAgentTasks parses @agent mentions from comment content and
// enqueues a task for each mentioned agent. Skips self-mentions, agents that
// are already the issue's assignee (handled by on_comment), and agents with
// on_mention trigger disabled.
// are already the issue's assignee (handled by on_comment), agents with
// on_mention trigger disabled, and private agents mentioned by non-owner
// members (only the agent owner or workspace admin/owner can mention a
// private agent).
// Note: no status gate here — @mention is an explicit action and should work
// even on done/cancelled issues (the agent can reopen the issue if needed).
func (h *Handler) enqueueMentionedAgentTasks(ctx context.Context, issue db.Issue, comment db.Comment, authorType, authorID string) {
wsID := uuidToString(issue.WorkspaceID)
mentions := util.ParseMentions(comment.Content)
for _, m := range mentions {
if m.Type != "agent" {
@ -266,8 +269,23 @@ func (h *Handler) enqueueMentionedAgentTasks(ctx context.Context, issue db.Issue
issue.AssigneeID.Valid && uuidToString(issue.AssigneeID) == m.ID {
continue
}
// Load the agent to check visibility and trigger config.
agent, err := h.Queries.GetAgent(ctx, agentUUID)
if err != nil || !agent.RuntimeID.Valid {
continue
}
// Private agents can only be mentioned by the agent owner or workspace admin/owner.
if agent.Visibility == "private" && authorType == "member" {
isOwner := uuidToString(agent.OwnerID) == authorID
if !isOwner {
member, err := h.getWorkspaceMember(ctx, authorID, wsID)
if err != nil || !roleAllowed(member.Role, "owner", "admin") {
continue
}
}
}
// Check if the agent has on_mention trigger enabled.
if !h.isAgentMentionTriggerEnabled(ctx, agentUUID) {
if !agentHasTriggerEnabled(agent.Triggers, "on_mention") {
continue
}
// Dedup: skip if this agent already has a pending task for this issue.