diff --git a/server/internal/handler/agent.go b/server/internal/handler/agent.go index c69dfe3a..d81c47e9 100644 --- a/server/internal/handler/agent.go +++ b/server/internal/handler/agent.go @@ -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) } diff --git a/server/internal/handler/comment.go b/server/internal/handler/comment.go index c131e45a..9bd353e7 100644 --- a/server/internal/handler/comment.go +++ b/server/internal/handler/comment.go @@ -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.