plugin side confirmed
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user