From bedf4a05c82cff86bc6cb000ae2e98a13aaa4781 Mon Sep 17 00:00:00 2001 From: Jiayuan Date: Tue, 31 Mar 2026 14:34:04 +0800 Subject: [PATCH 1/3] fix(agent): fix agent visibility defaults and permission model - Change DB default for agent visibility from 'workspace' to 'private' - Fix canManageAgent: workspace agents are now manageable by all members, private agents remain restricted to owner/admin - Add private agent visibility check to BatchAssigneePicker (was missing) --- .../components/batch-action-toolbar.tsx | 56 +++++++++++++------ server/internal/handler/agent.go | 13 ++--- .../028_agent_default_private.down.sql | 1 + .../028_agent_default_private.up.sql | 1 + 4 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 server/migrations/028_agent_default_private.down.sql create mode 100644 server/migrations/028_agent_default_private.up.sql 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'; From f69aa93a757ea09e61bb2ff0182904085d15907e Mon Sep 17 00:00:00 2001 From: Jiayuan Date: Tue, 31 Mar 2026 14:40:53 +0800 Subject: [PATCH 2/3] feat(agents): add Settings tab for editing agent visibility and properties Adds a Settings tab to the agent detail panel with: - Name and description editing - Visibility toggle (workspace/private) matching the create dialog pattern - Max concurrent tasks configuration - Runtime info display (read-only) --- apps/web/app/(dashboard)/agents/page.tsx | 143 ++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index 902cbcf4..dce100c4 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -27,6 +27,7 @@ import { ChevronDown, Globe, Lock, + Settings, } from "lucide-react"; import type { Agent, @@ -1151,11 +1152,143 @@ function TasksTab({ agent }: { agent: Agent }) { ); } +// --------------------------------------------------------------------------- +// Settings Tab +// --------------------------------------------------------------------------- + +function SettingsTab({ + agent, + runtimes, + onSave, +}: { + agent: Agent; + runtimes: RuntimeDevice[]; + onSave: (updates: Partial) => Promise; +}) { + const [name, setName] = useState(agent.name); + const [description, setDescription] = useState(agent.description ?? ""); + const [visibility, setVisibility] = useState(agent.visibility); + const [maxTasks, setMaxTasks] = useState(agent.max_concurrent_tasks); + const [saving, setSaving] = useState(false); + + const dirty = + name !== agent.name || + description !== (agent.description ?? "") || + visibility !== agent.visibility || + maxTasks !== agent.max_concurrent_tasks; + + const handleSave = async () => { + if (!name.trim()) { + toast.error("Name is required"); + return; + } + setSaving(true); + try { + await onSave({ name: name.trim(), description, visibility, max_concurrent_tasks: maxTasks }); + toast.success("Settings saved"); + } catch { + toast.error("Failed to save settings"); + } finally { + setSaving(false); + } + }; + + const runtimeDevice = runtimes.find((r) => r.id === agent.runtime_id); + + return ( +
+
+ + setName(e.target.value)} + className="mt-1" + /> +
+ +
+ + setDescription(e.target.value)} + placeholder="What does this agent do?" + className="mt-1" + /> +
+ +
+ +
+ + +
+
+ +
+ + setMaxTasks(Number(e.target.value))} + className="mt-1 w-24" + /> +
+ +
+ +
+ {agent.runtime_mode === "cloud" ? ( + + ) : ( + + )} + {runtimeDevice?.name ?? (agent.runtime_mode === "cloud" ? "Cloud" : "Local")} +
+
+ + +
+ ); +} + // --------------------------------------------------------------------------- // Agent Detail // --------------------------------------------------------------------------- -type DetailTab = "instructions" | "skills" | "tools" | "triggers" | "tasks"; +type DetailTab = "instructions" | "skills" | "tools" | "triggers" | "tasks" | "settings"; const detailTabs: { id: DetailTab; label: string; icon: typeof FileText }[] = [ { id: "instructions", label: "Instructions", icon: FileText }, @@ -1163,6 +1296,7 @@ const detailTabs: { id: DetailTab; label: string; icon: typeof FileText }[] = [ { id: "tools", label: "Tools", icon: Wrench }, { id: "triggers", label: "Triggers", icon: Timer }, { id: "tasks", label: "Tasks", icon: ListTodo }, + { id: "settings", label: "Settings", icon: Settings }, ]; function AgentDetail({ @@ -1267,6 +1401,13 @@ function AgentDetail({ /> )} {activeTab === "tasks" && } + {activeTab === "settings" && ( + onUpdate(agent.id, updates)} + /> + )} {/* Delete Confirmation */} From feb5f576622de6a0e6f3bce69378bc280102d3a8 Mon Sep 17 00:00:00 2001 From: Jiayuan Date: Tue, 31 Mar 2026 15:47:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20merge=20main=20and=20renumber=20migr?= =?UTF-8?q?ation=20028=20=E2=86=92=20030?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main now has 028 (task_trigger_comment) and 029 (daemon_token). Renumber agent_default_private migration to 030 to avoid conflict. --- ...efault_private.down.sql => 030_agent_default_private.down.sql} | 0 ...nt_default_private.up.sql => 030_agent_default_private.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename server/migrations/{028_agent_default_private.down.sql => 030_agent_default_private.down.sql} (100%) rename server/migrations/{028_agent_default_private.up.sql => 030_agent_default_private.up.sql} (100%) diff --git a/server/migrations/028_agent_default_private.down.sql b/server/migrations/030_agent_default_private.down.sql similarity index 100% rename from server/migrations/028_agent_default_private.down.sql rename to server/migrations/030_agent_default_private.down.sql diff --git a/server/migrations/028_agent_default_private.up.sql b/server/migrations/030_agent_default_private.up.sql similarity index 100% rename from server/migrations/028_agent_default_private.up.sql rename to server/migrations/030_agent_default_private.up.sql