plugin side confirmed

This commit is contained in:
2026-03-17 14:33:32 -04:00
parent 39a8ddef97
commit ecd78f7528
6 changed files with 336 additions and 100 deletions

View File

@@ -1,18 +1,24 @@
// Shared permission keys for agent permissions plugin import { PERMISSION_KEYS } from "@paperclipai/shared";
// Only the single real enforced permission is exposed here.
export const PERMISSION_KEYS = [
"canCreateAgents",
] as const;
export { PERMISSION_KEYS };
export type PermissionKey = typeof PERMISSION_KEYS[number]; export type PermissionKey = typeof PERMISSION_KEYS[number];
export const PERMISSION_LABELS: Record<PermissionKey, string> = { export const PERMISSION_LABELS: Record<PermissionKey, string> = {
"canCreateAgents": "Can Hire Agents", "agents:create": "Can Hire Agents",
"users:invite": "Can Invite Users",
"users:manage_permissions": "Can Manage User Permissions",
"tasks:assign": "Can Assign Agents to Tasks",
"tasks:assign_scope": "Can Assign Agents (Scoped)",
"joins:approve": "Can Approve Join Requests",
}; };
export const PERMISSION_DESCRIPTIONS: Record<PermissionKey, string> = { export const PERMISSION_DESCRIPTIONS: Record<PermissionKey, string> = {
"canCreateAgents": "Allows this agent to create (hire) new agents", "agents:create": "Create (hire) new agents in the company",
"users:invite": "Invite new users to join the company",
"users:manage_permissions": "Modify permission grants for other users",
"tasks:assign": "Assign agents to tasks and issues",
"tasks:assign_scope": "Assign agents within a specific scope",
"joins:approve": "Approve or reject pending join requests",
}; };
// Pagination constants // Pagination constants

View File

@@ -11,6 +11,8 @@ const manifest: PaperclipPluginManifestV1 = {
capabilities: [ capabilities: [
"agents.read", "agents.read",
"agents.update-permissions", "agents.update-permissions",
"agents.grants.read",
"agents.grants.write",
"ui.detailTab.register", "ui.detailTab.register",
"ui.page.register", "ui.page.register",
"instance.settings.register" "instance.settings.register"

View File

@@ -1,12 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui"; import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
import type { PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui"; import type { PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui";
import { PERMISSION_LABELS, PERMISSION_DESCRIPTIONS } from "../constants"; import { PERMISSION_KEYS, PERMISSION_LABELS, PERMISSION_DESCRIPTIONS, type PermissionKey } from "../constants";
interface AgentPermissionsData { interface AgentPermissionsData {
agentId: string; agentId: string;
agentName: string; agentName: string;
canCreateAgents: boolean; canCreateAgents: boolean;
grants: string[];
} }
export function AgentPermissionsTab({ context }: PluginDetailTabProps) { export function AgentPermissionsTab({ context }: PluginDetailTabProps) {
@@ -17,10 +18,47 @@ export function AgentPermissionsTab({ context }: PluginDetailTabProps) {
{ agentId, companyId } { agentId, companyId }
); );
const togglePermission = usePluginAction("toggle-agent-permission"); const toggleLegacy = usePluginAction("toggle-can-create-agents");
const [updating, setUpdating] = useState(false); const toggleGrant = usePluginAction("toggle-permission");
const [updating, setUpdating] = useState<string | null>(null);
const [lastError, setLastError] = useState<Error | null>(null); const [lastError, setLastError] = useState<Error | null>(null);
async function handleToggleLegacy(currentEnabled: boolean) {
if (!companyId) return;
setLastError(null);
setUpdating("legacy");
try {
await toggleLegacy({ agentId, companyId, enabled: !currentEnabled });
await refresh();
} catch (err) {
const e = err instanceof Error ? err : new Error(String(err));
setLastError(e);
console.error("Failed to toggle permission:", e);
} finally {
setUpdating(null);
}
}
async function handleToggleGrant(key: PermissionKey, currentEnabled: boolean) {
if (!companyId) return;
setLastError(null);
setUpdating(key);
try {
await toggleGrant({ agentId, companyId, permissionKey: key, enabled: !currentEnabled });
await refresh();
} catch (err) {
const e = err instanceof Error ? err : new Error(String(err));
setLastError(e);
console.error("Failed to toggle grant:", e);
} finally {
setUpdating(null);
}
}
function hasGrant(key: PermissionKey): boolean {
return data?.grants.includes(key) ?? false;
}
if (loading) return <div style={{ padding: "1rem" }}>Loading permissions...</div>; if (loading) return <div style={{ padding: "1rem" }}>Loading permissions...</div>;
if (error) return ( if (error) return (
<div role="alert" style={{ padding: "1rem", color: "#c00" }}> <div role="alert" style={{ padding: "1rem", color: "#c00" }}>
@@ -29,21 +67,6 @@ export function AgentPermissionsTab({ context }: PluginDetailTabProps) {
); );
if (!data) return <div style={{ padding: "1rem" }}>No permissions data available</div>; if (!data) return <div style={{ padding: "1rem" }}>No permissions data available</div>;
async function handleToggle(currentEnabled: boolean) {
setLastError(null);
setUpdating(true);
try {
await togglePermission({ agentId, companyId, enabled: !currentEnabled });
await refresh();
} catch (err) {
const e = err instanceof Error ? err : new Error(String(err));
setLastError(e);
console.error("Failed to toggle permission:", e);
} finally {
setUpdating(false);
}
}
return ( return (
<div style={{ padding: "1rem", maxWidth: "600px" }}> <div style={{ padding: "1rem", maxWidth: "600px" }}>
<h2 id="permissions-heading" style={{ marginBottom: "1.5rem", fontSize: "1.25rem", fontWeight: 600 }}>Agent Permissions</h2> <h2 id="permissions-heading" style={{ marginBottom: "1.5rem", fontSize: "1.25rem", fontWeight: 600 }}>Agent Permissions</h2>
@@ -72,43 +95,78 @@ export function AgentPermissionsTab({ context }: PluginDetailTabProps) {
<legend style={{ display: "none" }}>Permission toggles</legend> <legend style={{ display: "none" }}>Permission toggles</legend>
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}> <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<label <label
htmlFor="perm-canCreateAgents" htmlFor="perm-legacy"
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "0.75rem", gap: "0.75rem",
cursor: updating ? "wait" : "pointer", cursor: updating === "legacy" ? "wait" : "pointer",
padding: "0.5rem", padding: "0.5rem",
borderRadius: "4px", borderRadius: "4px",
backgroundColor: updating ? "#f5f5f5" : "transparent", backgroundColor: updating === "legacy" ? "#f5f5f5" : "transparent",
transition: "background-color 0.2s" transition: "background-color 0.2s"
}} }}
> >
<input <input
id="perm-canCreateAgents" id="perm-legacy"
type="checkbox" type="checkbox"
checked={data.canCreateAgents} checked={data.canCreateAgents}
onChange={() => handleToggle(data.canCreateAgents)} onChange={() => handleToggleLegacy(data.canCreateAgents)}
disabled={updating} disabled={updating !== null}
style={{ width: "18px", height: "18px", cursor: updating ? "wait" : "pointer" }} style={{ width: "18px", height: "18px", cursor: updating === "legacy" ? "wait" : "pointer" }}
/> />
<span> <span>
<span style={{ fontWeight: 500, display: "block" }}> <span style={{ fontWeight: 500, display: "block" }}>Can Hire Agents (legacy)</span>
{PERMISSION_LABELS["canCreateAgents"]}
</span>
<span style={{ color: "#666", fontSize: "0.875rem" }}> <span style={{ color: "#666", fontSize: "0.875rem" }}>
{PERMISSION_DESCRIPTIONS["canCreateAgents"]} Allows this agent to create (hire) new agents. Also grants task assignment rights.
</span> </span>
</span> </span>
{updating && ( {updating === "legacy" && (
<span <span style={{ color: "#888", fontSize: "0.875rem" }} role="status">
style={{ color: "#888", fontSize: "0.875rem" }}
role="status"
>
updating... updating...
</span> </span>
)} )}
</label> </label>
{PERMISSION_KEYS.map((key) => {
const enabled = hasGrant(key);
return (
<label
key={key}
htmlFor={`perm-${key}`}
style={{
display: "flex",
alignItems: "center",
gap: "0.75rem",
cursor: updating === key ? "wait" : "pointer",
padding: "0.5rem",
borderRadius: "4px",
backgroundColor: updating === key ? "#f5f5f5" : "transparent",
transition: "background-color 0.2s"
}}
>
<input
id={`perm-${key}`}
type="checkbox"
checked={enabled}
onChange={() => handleToggleGrant(key, enabled)}
disabled={updating !== null}
style={{ width: "18px", height: "18px", cursor: updating === key ? "wait" : "pointer" }}
/>
<span>
<span style={{ fontWeight: 500, display: "block" }}>{PERMISSION_LABELS[key]}</span>
<span style={{ color: "#666", fontSize: "0.875rem" }}>
{PERMISSION_DESCRIPTIONS[key]}
</span>
</span>
{updating === key && (
<span style={{ color: "#888", fontSize: "0.875rem" }} role="status">
updating...
</span>
)}
</label>
);
})}
</div> </div>
</fieldset> </fieldset>
</div> </div>

View File

@@ -1,12 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { usePluginData, usePluginAction, useHostContext } from "@paperclipai/plugin-sdk/ui"; import { usePluginData, usePluginAction, useHostContext } from "@paperclipai/plugin-sdk/ui";
import type { PluginPageProps } from "@paperclipai/plugin-sdk/ui"; import type { PluginPageProps } from "@paperclipai/plugin-sdk/ui";
import { PERMISSION_LABELS, PERMISSION_DESCRIPTIONS } from "../constants"; import { PERMISSION_KEYS, PERMISSION_LABELS, PERMISSION_DESCRIPTIONS, type PermissionKey } from "../constants";
interface AgentPermissionsSummary { interface AgentPermissionsSummary {
agentId: string; agentId: string;
agentName: string; agentName: string;
canCreateAgents: boolean; canCreateAgents: boolean;
grants: string[];
} }
export function PermissionsPage(_props: PluginPageProps) { export function PermissionsPage(_props: PluginPageProps) {
@@ -17,21 +18,39 @@ export function PermissionsPage(_props: PluginPageProps) {
companyId ? { companyId, limit: 100 } : {} companyId ? { companyId, limit: 100 } : {}
); );
const togglePermission = usePluginAction("toggle-agent-permission"); const toggleLegacy = usePluginAction("toggle-can-create-agents");
const toggleGrant = usePluginAction("toggle-permission");
const [updating, setUpdating] = useState<string | null>(null); const [updating, setUpdating] = useState<string | null>(null);
const [lastError, setLastError] = useState<Error | null>(null); const [lastError, setLastError] = useState<Error | null>(null);
const [expandedAgents, setExpandedAgents] = useState<Set<string>>(new Set()); const [expandedAgents, setExpandedAgents] = useState<Set<string>>(new Set());
async function handleToggle(agentId: string, currentEnabled: boolean) { async function handleToggleLegacy(agentId: string, currentEnabled: boolean) {
if (!companyId) return;
setLastError(null); setLastError(null);
setUpdating(agentId); setUpdating(`${agentId}:legacy`);
try { try {
await togglePermission({ agentId, companyId, enabled: !currentEnabled }); await toggleLegacy({ agentId, companyId, enabled: !currentEnabled });
await refresh(); await refresh();
} catch (err) { } catch (err) {
const error = err instanceof Error ? err : new Error(String(err)); const e = err instanceof Error ? err : new Error(String(err));
setLastError(error); setLastError(e);
console.error("Failed to toggle permission:", error); console.error("Failed to toggle permission:", e);
} finally {
setUpdating(null);
}
}
async function handleToggleGrant(agentId: string, key: PermissionKey, currentEnabled: boolean) {
if (!companyId) return;
setLastError(null);
setUpdating(`${agentId}:${key}`);
try {
await toggleGrant({ agentId, companyId, permissionKey: key, enabled: !currentEnabled });
await refresh();
} catch (err) {
const e = err instanceof Error ? err : new Error(String(err));
setLastError(e);
console.error("Failed to toggle grant:", e);
} finally { } finally {
setUpdating(null); setUpdating(null);
} }
@@ -49,6 +68,10 @@ export function PermissionsPage(_props: PluginPageProps) {
}); });
} }
function hasGrant(agent: AgentPermissionsSummary, key: PermissionKey): boolean {
return agent.grants.includes(key);
}
if (loading) { if (loading) {
return ( return (
<div style={{ padding: "2rem", textAlign: "center" }}> <div style={{ padding: "2rem", textAlign: "center" }}>
@@ -105,7 +128,7 @@ export function PermissionsPage(_props: PluginPageProps) {
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}> <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
{agentsData.map(agent => { {agentsData.map(agent => {
const isExpanded = expandedAgents.has(agent.agentId); const isExpanded = expandedAgents.has(agent.agentId);
const isUpdating = updating === agent.agentId; const hasAny = agent.canCreateAgents || agent.grants.length > 0;
return ( return (
<div <div
@@ -146,7 +169,7 @@ export function PermissionsPage(_props: PluginPageProps) {
</span> </span>
<span>{agent.agentName}</span> <span>{agent.agentName}</span>
</div> </div>
{agent.canCreateAgents && ( {hasAny && (
<span style={{ <span style={{
fontSize: "0.75rem", fontSize: "0.75rem",
backgroundColor: "#e5e7eb", backgroundColor: "#e5e7eb",
@@ -154,7 +177,7 @@ export function PermissionsPage(_props: PluginPageProps) {
padding: "0.125rem 0.5rem", padding: "0.125rem 0.5rem",
borderRadius: "999px" borderRadius: "999px"
}}> }}>
can hire {agent.grants.length + (agent.canCreateAgents ? 1 : 0)} permissions
</span> </span>
)} )}
</button> </button>
@@ -163,9 +186,49 @@ export function PermissionsPage(_props: PluginPageProps) {
<div style={{ padding: "0 1.25rem 1.25rem" }}> <div style={{ padding: "0 1.25rem 1.25rem" }}>
<fieldset style={{ border: "none", padding: 0, margin: 0 }}> <fieldset style={{ border: "none", padding: 0, margin: 0 }}>
<legend style={{ display: "none" }}>Permission toggles for {agent.agentName}</legend> <legend style={{ display: "none" }}>Permission toggles for {agent.agentName}</legend>
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem", marginTop: "0.5rem" }}> <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<label <label
htmlFor={`perm-${agent.agentId}-canCreateAgents`} htmlFor={`perm-${agent.agentId}-canCreateAgents`}
style={{
display: "flex",
alignItems: "center",
gap: "0.75rem",
cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer",
padding: "0.75rem",
borderRadius: "6px",
backgroundColor: updating === `${agent.agentId}:legacy` ? "#f9fafb" : "#fafafa",
border: "1px solid #e5e7eb",
transition: "background-color 0.2s"
}}
>
<input
id={`perm-${agent.agentId}-canCreateAgents`}
type="checkbox"
checked={agent.canCreateAgents}
onChange={() => handleToggleLegacy(agent.agentId, agent.canCreateAgents)}
disabled={updating !== null}
style={{ width: "18px", height: "18px", cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer" }}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500 }}>Can Hire Agents (legacy)</div>
<div style={{ fontSize: "0.75rem", color: "#666" }}>
Allows this agent to create (hire) new agents. Also grants task assignment rights.
</div>
</div>
{updating === `${agent.agentId}:legacy` && (
<span style={{ color: "#888", fontSize: "0.75rem" }} role="status">
updating...
</span>
)}
</label>
{PERMISSION_KEYS.map((key) => {
const enabled = hasGrant(agent, key);
const isUpdating = updating === `${agent.agentId}:${key}`;
return (
<label
key={key}
htmlFor={`perm-${agent.agentId}-${key}`}
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -179,17 +242,17 @@ export function PermissionsPage(_props: PluginPageProps) {
}} }}
> >
<input <input
id={`perm-${agent.agentId}-canCreateAgents`} id={`perm-${agent.agentId}-${key}`}
type="checkbox" type="checkbox"
checked={agent.canCreateAgents} checked={enabled}
onChange={() => handleToggle(agent.agentId, agent.canCreateAgents)} onChange={() => handleToggleGrant(agent.agentId, key, enabled)}
disabled={updating !== null} disabled={updating !== null}
style={{ width: "18px", height: "18px", cursor: isUpdating ? "wait" : "pointer" }} style={{ width: "18px", height: "18px", cursor: isUpdating ? "wait" : "pointer" }}
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ fontWeight: 500 }}>{PERMISSION_LABELS["canCreateAgents"]}</div> <div style={{ fontWeight: 500 }}>{PERMISSION_LABELS[key]}</div>
<div style={{ fontSize: "0.75rem", color: "#666" }}> <div style={{ fontSize: "0.75rem", color: "#666" }}>
{PERMISSION_DESCRIPTIONS["canCreateAgents"]} {PERMISSION_DESCRIPTIONS[key]}
</div> </div>
</div> </div>
{isUpdating && ( {isUpdating && (
@@ -198,6 +261,8 @@ export function PermissionsPage(_props: PluginPageProps) {
</span> </span>
)} )}
</label> </label>
);
})}
</div> </div>
</fieldset> </fieldset>
</div> </div>

View File

@@ -1,12 +1,13 @@
import { useState } from "react"; import { useState } from "react";
import { usePluginData, usePluginAction, usePluginToast } from "@paperclipai/plugin-sdk/ui"; import { usePluginData, usePluginAction, usePluginToast } from "@paperclipai/plugin-sdk/ui";
import type { PluginSettingsPageProps } from "@paperclipai/plugin-sdk/ui"; import type { PluginSettingsPageProps } from "@paperclipai/plugin-sdk/ui";
import { PERMISSION_LABELS, PERMISSION_DESCRIPTIONS } from "../constants"; import { PERMISSION_KEYS, PERMISSION_LABELS, PERMISSION_DESCRIPTIONS, type PermissionKey } from "../constants";
interface AgentPermissionsSummary { interface AgentPermissionsSummary {
agentId: string; agentId: string;
agentName: string; agentName: string;
canCreateAgents: boolean; canCreateAgents: boolean;
grants: string[];
} }
const layoutStyle = { const layoutStyle = {
@@ -78,7 +79,8 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
companyId ? { companyId, limit: 100 } : {} companyId ? { companyId, limit: 100 } : {}
); );
const togglePermission = usePluginAction("toggle-agent-permission"); const toggleLegacy = usePluginAction("toggle-can-create-agents");
const toggleGrant = usePluginAction("toggle-permission");
const toast = usePluginToast(); const toast = usePluginToast();
const [expandedAgents, setExpandedAgents] = useState<Set<string>>(new Set()); const [expandedAgents, setExpandedAgents] = useState<Set<string>>(new Set());
@@ -96,14 +98,15 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
}); });
} }
async function handleToggle(agentId: string, currentEnabled: boolean) { async function handleToggleLegacy(agentId: string, currentEnabled: boolean) {
setUpdating(agentId); if (!companyId) return;
setUpdating(`${agentId}:legacy`);
try { try {
await togglePermission({ agentId, companyId, enabled: !currentEnabled }); await toggleLegacy({ agentId, companyId, enabled: !currentEnabled });
await refresh(); await refresh();
toast({ toast({
title: "Permission updated", title: "Permission updated",
body: `${PERMISSION_LABELS["canCreateAgents"]}: ${!currentEnabled ? "allowed" : "denied"}`, body: `Can Hire Agents: ${!currentEnabled ? "allowed" : "denied"}`,
tone: "success", tone: "success",
}); });
} catch (err) { } catch (err) {
@@ -118,6 +121,33 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
} }
} }
async function handleToggleGrant(agentId: string, key: PermissionKey, currentEnabled: boolean) {
if (!companyId) return;
setUpdating(`${agentId}:${key}`);
try {
await toggleGrant({ agentId, companyId, permissionKey: key, enabled: !currentEnabled });
await refresh();
toast({
title: "Permission updated",
body: `${PERMISSION_LABELS[key]}: ${!currentEnabled ? "allowed" : "denied"}`,
tone: "success",
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
toast({
title: "Failed to update permission",
body: message,
tone: "error",
});
} finally {
setUpdating(null);
}
}
function hasGrant(agent: AgentPermissionsSummary, key: PermissionKey): boolean {
return agent.grants.includes(key);
}
if (!companyId) { if (!companyId) {
return ( return (
<div style={layoutStyle}> <div style={layoutStyle}>
@@ -162,14 +192,14 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
<div style={layoutStyle}> <div style={layoutStyle}>
<div style={{ display: "grid", gap: "6px" }}> <div style={{ display: "grid", gap: "6px" }}>
<div style={mutedTextStyle}> <div style={mutedTextStyle}>
Toggle hiring permission to allow or deny each agent from creating new agents. Manage permission grants for each agent. Toggle hiring permission and other capabilities.
</div> </div>
</div> </div>
<div style={{ display: "grid", gap: "10px" }}> <div style={{ display: "grid", gap: "10px" }}>
{agentsData.map((agent) => { {agentsData.map((agent) => {
const isExpanded = expandedAgents.has(agent.agentId); const isExpanded = expandedAgents.has(agent.agentId);
const isUpdating = updating === agent.agentId; const hasAny = agent.canCreateAgents || agent.grants.length > 0;
return ( return (
<div key={agent.agentId} style={cardStyle}> <div key={agent.agentId} style={cardStyle}>
@@ -178,9 +208,9 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
<div style={{ display: "grid", gap: "2px", flex: 1 }}> <div style={{ display: "grid", gap: "2px", flex: 1 }}>
<strong style={{ fontSize: "13px" }}>{agent.agentName}</strong> <strong style={{ fontSize: "13px" }}>{agent.agentName}</strong>
</div> </div>
{agent.canCreateAgents && ( {hasAny && (
<span style={{ ...mutedTextStyle, fontSize: "11px" }}> <span style={{ ...mutedTextStyle, fontSize: "11px" }}>
can hire {agent.grants.length + (agent.canCreateAgents ? 1 : 0)} permissions
</span> </span>
)} )}
</div> </div>
@@ -190,27 +220,59 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
<div style={{ display: "grid", gap: "8px" }}> <div style={{ display: "grid", gap: "8px" }}>
<div style={permissionRowStyle}> <div style={permissionRowStyle}>
<div style={{ display: "grid", gap: "2px", flex: 1 }}> <div style={{ display: "grid", gap: "2px", flex: 1 }}>
<div style={{ fontSize: "12px", fontWeight: 500 }}>{PERMISSION_LABELS["canCreateAgents"]}</div> <div style={{ fontSize: "12px", fontWeight: 500 }}>Can Hire Agents (legacy)</div>
<div style={{ ...mutedTextStyle, fontSize: "11px" }}>{PERMISSION_DESCRIPTIONS["canCreateAgents"]}</div> <div style={{ ...mutedTextStyle, fontSize: "11px" }}>
Allows this agent to create (hire) new agents. Also grants task assignment rights.
</div> </div>
<label style={{ display: "flex", alignItems: "center", gap: "6px", cursor: isUpdating ? "wait" : "pointer" }}> </div>
<label style={{ display: "flex", alignItems: "center", gap: "6px", cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer" }}>
<input <input
type="checkbox" type="checkbox"
checked={agent.canCreateAgents} checked={agent.canCreateAgents}
disabled={updating !== null} disabled={updating !== null}
onChange={() => handleToggle(agent.agentId, agent.canCreateAgents)} onChange={() => handleToggleLegacy(agent.agentId, agent.canCreateAgents)}
style={{ style={{
width: "16px", width: "16px",
height: "16px", height: "16px",
accentColor: "var(--foreground)", accentColor: "var(--foreground)",
cursor: isUpdating ? "wait" : "pointer", cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer",
}} }}
/> />
<span style={{ fontSize: "11px", opacity: 0.8 }}> <span style={{ fontSize: "11px", opacity: 0.8 }}>
{isUpdating ? "saving…" : agent.canCreateAgents ? "Allowed" : "Denied"} {updating === `${agent.agentId}:legacy` ? "saving…" : agent.canCreateAgents ? "Allowed" : "Denied"}
</span> </span>
</label> </label>
</div> </div>
{PERMISSION_KEYS.map((key) => {
const enabled = hasGrant(agent, key);
const isKeyUpdating = updating === `${agent.agentId}:${key}`;
return (
<div key={key} style={permissionRowStyle}>
<div style={{ display: "grid", gap: "2px", flex: 1 }}>
<div style={{ fontSize: "12px", fontWeight: 500 }}>{PERMISSION_LABELS[key]}</div>
<div style={{ ...mutedTextStyle, fontSize: "11px" }}>{PERMISSION_DESCRIPTIONS[key]}</div>
</div>
<label style={{ display: "flex", alignItems: "center", gap: "6px", cursor: isKeyUpdating ? "wait" : "pointer" }}>
<input
type="checkbox"
checked={enabled}
disabled={updating !== null}
onChange={() => handleToggleGrant(agent.agentId, key, enabled)}
style={{
width: "16px",
height: "16px",
accentColor: "var(--foreground)",
cursor: isKeyUpdating ? "wait" : "pointer",
}}
/>
<span style={{ fontSize: "11px", opacity: 0.8 }}>
{isKeyUpdating ? "saving…" : enabled ? "Allowed" : "Denied"}
</span>
</label>
</div>
);
})}
</div> </div>
</div> </div>
) : null} ) : null}

View File

@@ -5,32 +5,30 @@ interface AgentPermissionsSummary {
agentId: string; agentId: string;
agentName: string; agentName: string;
canCreateAgents: boolean; canCreateAgents: boolean;
grants: string[];
} }
const plugin = definePlugin({ const plugin = definePlugin({
async setup(ctx) { async setup(ctx) {
// Per-agent data for the detail tab
ctx.data.register("agent-permissions", async (params) => { ctx.data.register("agent-permissions", async (params) => {
const agentId = typeof params.agentId === "string" ? params.agentId : ""; const agentId = typeof params.agentId === "string" ? params.agentId : "";
const companyId = typeof params.companyId === "string" ? params.companyId : ""; const companyId = typeof params.companyId === "string" ? params.companyId : "";
if (!agentId) return null; if (!agentId || !companyId) return null;
// companyId may be empty when called from the detail tab; we still try
const agent = companyId
? await ctx.agents.get(agentId, companyId)
: null;
const agent = await ctx.agents.get(agentId, companyId);
if (!agent) return null; if (!agent) return null;
const grants = await ctx.agents.getGrants(agentId, companyId);
return { return {
agentId: agent.id, agentId: agent.id,
agentName: agent.name, agentName: agent.name,
canCreateAgents: agent.permissions?.canCreateAgents ?? false, canCreateAgents: agent.permissions?.canCreateAgents ?? false,
grants,
}; };
}); });
// All-agents list for the permissions page and settings page
ctx.data.register("all-agents-permissions", async (params) => { ctx.data.register("all-agents-permissions", async (params) => {
const companyId = typeof params.companyId === "string" ? params.companyId : ""; const companyId = typeof params.companyId === "string" ? params.companyId : "";
const limit = Math.min( const limit = Math.min(
@@ -39,20 +37,65 @@ const plugin = definePlugin({
); );
const offset = Math.max(0, Number(params.offset) || 0); const offset = Math.max(0, Number(params.offset) || 0);
const agents = companyId if (!companyId) return [];
? await ctx.agents.list({ companyId, limit, offset })
: [];
const result: AgentPermissionsSummary[] = agents.map(agent => ({ const agents = await ctx.agents.list({ companyId, limit, offset });
const results: AgentPermissionsSummary[] = await Promise.all(
agents.map(async (agent) => {
let grants: string[] = [];
try {
grants = await ctx.agents.getGrants(agent.id, companyId);
} catch {
grants = [];
}
return {
agentId: agent.id, agentId: agent.id,
agentName: agent.name, agentName: agent.name,
canCreateAgents: agent.permissions?.canCreateAgents ?? false, canCreateAgents: agent.permissions?.canCreateAgents ?? false,
})); grants,
};
})
);
return result; return results;
}); });
ctx.actions.register("toggle-agent-permission", async (params) => { ctx.actions.register("toggle-permission", async (params) => {
const { agentId, companyId, permissionKey, enabled } = params as {
agentId: string;
companyId: string;
permissionKey: string;
enabled: boolean;
};
if (!agentId || typeof agentId !== "string") {
throw new Error("Invalid agentId: must be a non-empty string");
}
if (!companyId || typeof companyId !== "string") {
throw new Error("Invalid companyId: must be a non-empty string");
}
if (!permissionKey || typeof permissionKey !== "string") {
throw new Error("Invalid permissionKey: must be a non-empty string");
}
if (typeof enabled !== "boolean") {
throw new Error("Invalid enabled: must be a boolean");
}
const currentGrants = await ctx.agents.getGrants(agentId, companyId);
const grantSet = new Set(currentGrants);
if (enabled) {
grantSet.add(permissionKey);
} else {
grantSet.delete(permissionKey);
}
await ctx.agents.setGrants(agentId, companyId, [...grantSet]);
return { success: true };
});
ctx.actions.register("toggle-can-create-agents", async (params) => {
const { agentId, companyId, enabled } = params as { const { agentId, companyId, enabled } = params as {
agentId: string; agentId: string;
companyId: string; companyId: string;