diff --git a/apps/web/features/issues/components/batch-action-toolbar.tsx b/apps/web/features/issues/components/batch-action-toolbar.tsx
index edf56959..e52365f4 100644
--- a/apps/web/features/issues/components/batch-action-toolbar.tsx
+++ b/apps/web/features/issues/components/batch-action-toolbar.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
-import { X, Trash2, Bot, UserMinus } from "lucide-react";
+import { X, Trash2, Bot, Lock, UserMinus } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
@@ -19,8 +19,9 @@ import {
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
-import type { UpdateIssueRequest } from "@/shared/types";
+import type { Agent, UpdateIssueRequest } from "@/shared/types";
import { ALL_STATUSES, STATUS_CONFIG, PRIORITY_ORDER, PRIORITY_CONFIG } from "@/features/issues/config";
+import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore, useActorName } from "@/features/workspace";
import { useIssueStore } from "@/features/issues/store";
import { useIssueSelectionStore } from "@/features/issues/stores/selection-store";
@@ -206,6 +207,13 @@ export function BatchActionToolbar() {
);
}
+function canAssignAgent(agent: Agent, userId: string | undefined, memberRole: string | undefined): boolean {
+ if (agent.visibility !== "private") return true;
+ if (agent.owner_id === userId) return true;
+ if (memberRole === "owner" || memberRole === "admin") return true;
+ return false;
+}
+
function BatchAssigneePicker({
open,
onOpenChange,
@@ -218,10 +226,14 @@ function BatchAssigneePicker({
loading: boolean;
}) {
const [filter, setFilter] = useState("");
+ const user = useAuthStore((s) => s.user);
const members = useWorkspaceStore((s) => s.members);
const agents = useWorkspaceStore((s) => s.agents);
const { getActorInitials } = useActorName();
+ const currentMember = members.find((m) => m.user_id === user?.id);
+ const memberRole = currentMember?.role;
+
const query = filter.toLowerCase();
const filteredMembers = members.filter((m) =>
m.name.toLowerCase().includes(query),
@@ -297,22 +309,30 @@ function BatchAssigneePicker({
Agents
- {filteredAgents.map((a) => (
-
- ))}
+ {filteredAgents.map((a) => {
+ const allowed = canAssignAgent(a, user?.id, memberRole);
+ return (
+
+ );
+ })}
)}
diff --git a/server/internal/handler/agent.go b/server/internal/handler/agent.go
index 9ad40468..c510d6d9 100644
--- a/server/internal/handler/agent.go
+++ b/server/internal/handler/agent.go
@@ -326,24 +326,23 @@ type UpdateAgentRequest struct {
}
// canManageAgent checks whether the current user can update or delete an agent.
-// Workspace-visible agents require owner/admin role. Private agents additionally
-// require the user to be the agent's owner (or a workspace owner/admin).
+// Workspace-visible agents can be managed by any workspace member.
+// Private agents can only be managed by their owner or workspace owner/admin.
func (h *Handler) canManageAgent(w http.ResponseWriter, r *http.Request, agent db.Agent) bool {
wsID := uuidToString(agent.WorkspaceID)
member, ok := h.requireWorkspaceRole(w, r, wsID, "agent not found", "owner", "admin", "member")
if !ok {
return false
}
+ if agent.Visibility != "private" {
+ return true
+ }
isAdmin := roleAllowed(member.Role, "owner", "admin")
isAgentOwner := uuidToString(agent.OwnerID) == requestUserID(r)
- if agent.Visibility == "private" && !isAdmin && !isAgentOwner {
+ if !isAdmin && !isAgentOwner {
writeError(w, http.StatusForbidden, "only the agent owner can manage this private agent")
return false
}
- if agent.Visibility != "private" && !isAdmin && !isAgentOwner {
- writeError(w, http.StatusForbidden, "insufficient permissions")
- return false
- }
return true
}
diff --git a/server/migrations/028_agent_default_private.down.sql b/server/migrations/028_agent_default_private.down.sql
new file mode 100644
index 00000000..a7ea0a37
--- /dev/null
+++ b/server/migrations/028_agent_default_private.down.sql
@@ -0,0 +1 @@
+ALTER TABLE agent ALTER COLUMN visibility SET DEFAULT 'workspace';
diff --git a/server/migrations/028_agent_default_private.up.sql b/server/migrations/028_agent_default_private.up.sql
new file mode 100644
index 00000000..7b85faef
--- /dev/null
+++ b/server/migrations/028_agent_default_private.up.sql
@@ -0,0 +1 @@
+ALTER TABLE agent ALTER COLUMN visibility SET DEFAULT 'private';