plugin side confirmed
This commit is contained in:
@@ -1,21 +1,27 @@
|
||||
// Shared permission keys for agent permissions plugin
|
||||
// Only the single real enforced permission is exposed here.
|
||||
|
||||
export const PERMISSION_KEYS = [
|
||||
"canCreateAgents",
|
||||
] as const;
|
||||
import { PERMISSION_KEYS } from "@paperclipai/shared";
|
||||
|
||||
export { PERMISSION_KEYS };
|
||||
export type PermissionKey = typeof PERMISSION_KEYS[number];
|
||||
|
||||
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> = {
|
||||
"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
|
||||
export const DEFAULT_PAGE_LIMIT = 50;
|
||||
export const MAX_PAGE_LIMIT = 200;
|
||||
export const SIDEBAR_PREVIEW_LIMIT = 5;
|
||||
export const SIDEBAR_PREVIEW_LIMIT = 5;
|
||||
@@ -11,6 +11,8 @@ const manifest: PaperclipPluginManifestV1 = {
|
||||
capabilities: [
|
||||
"agents.read",
|
||||
"agents.update-permissions",
|
||||
"agents.grants.read",
|
||||
"agents.grants.write",
|
||||
"ui.detailTab.register",
|
||||
"ui.page.register",
|
||||
"instance.settings.register"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { usePluginData, usePluginAction } 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 {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
canCreateAgents: boolean;
|
||||
grants: string[];
|
||||
}
|
||||
|
||||
export function AgentPermissionsTab({ context }: PluginDetailTabProps) {
|
||||
@@ -17,10 +18,47 @@ export function AgentPermissionsTab({ context }: PluginDetailTabProps) {
|
||||
{ agentId, companyId }
|
||||
);
|
||||
|
||||
const togglePermission = usePluginAction("toggle-agent-permission");
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const toggleLegacy = usePluginAction("toggle-can-create-agents");
|
||||
const toggleGrant = usePluginAction("toggle-permission");
|
||||
const [updating, setUpdating] = useState<string | 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 (error) return (
|
||||
<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>;
|
||||
|
||||
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 (
|
||||
<div style={{ padding: "1rem", maxWidth: "600px" }}>
|
||||
<h2 id="permissions-heading" style={{ marginBottom: "1.5rem", fontSize: "1.25rem", fontWeight: 600 }}>Agent Permissions</h2>
|
||||
@@ -72,45 +95,80 @@ export function AgentPermissionsTab({ context }: PluginDetailTabProps) {
|
||||
<legend style={{ display: "none" }}>Permission toggles</legend>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
|
||||
<label
|
||||
htmlFor="perm-canCreateAgents"
|
||||
htmlFor="perm-legacy"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
cursor: updating ? "wait" : "pointer",
|
||||
cursor: updating === "legacy" ? "wait" : "pointer",
|
||||
padding: "0.5rem",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: updating ? "#f5f5f5" : "transparent",
|
||||
backgroundColor: updating === "legacy" ? "#f5f5f5" : "transparent",
|
||||
transition: "background-color 0.2s"
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="perm-canCreateAgents"
|
||||
id="perm-legacy"
|
||||
type="checkbox"
|
||||
checked={data.canCreateAgents}
|
||||
onChange={() => handleToggle(data.canCreateAgents)}
|
||||
disabled={updating}
|
||||
style={{ width: "18px", height: "18px", cursor: updating ? "wait" : "pointer" }}
|
||||
onChange={() => handleToggleLegacy(data.canCreateAgents)}
|
||||
disabled={updating !== null}
|
||||
style={{ width: "18px", height: "18px", cursor: updating === "legacy" ? "wait" : "pointer" }}
|
||||
/>
|
||||
<span>
|
||||
<span style={{ fontWeight: 500, display: "block" }}>
|
||||
{PERMISSION_LABELS["canCreateAgents"]}
|
||||
</span>
|
||||
<span style={{ fontWeight: 500, display: "block" }}>Can Hire Agents (legacy)</span>
|
||||
<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>
|
||||
{updating && (
|
||||
<span
|
||||
style={{ color: "#888", fontSize: "0.875rem" }}
|
||||
role="status"
|
||||
>
|
||||
{updating === "legacy" && (
|
||||
<span style={{ color: "#888", fontSize: "0.875rem" }} role="status">
|
||||
updating...
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { usePluginData, usePluginAction, useHostContext } 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 {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
canCreateAgents: boolean;
|
||||
grants: string[];
|
||||
}
|
||||
|
||||
export function PermissionsPage(_props: PluginPageProps) {
|
||||
@@ -17,21 +18,39 @@ export function PermissionsPage(_props: PluginPageProps) {
|
||||
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 [lastError, setLastError] = useState<Error | null>(null);
|
||||
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);
|
||||
setUpdating(agentId);
|
||||
setUpdating(`${agentId}:legacy`);
|
||||
try {
|
||||
await togglePermission({ agentId, companyId, enabled: !currentEnabled });
|
||||
await toggleLegacy({ agentId, companyId, enabled: !currentEnabled });
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
setLastError(error);
|
||||
console.error("Failed to toggle permission:", error);
|
||||
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(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 {
|
||||
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) {
|
||||
return (
|
||||
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||
@@ -105,7 +128,7 @@ export function PermissionsPage(_props: PluginPageProps) {
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||
{agentsData.map(agent => {
|
||||
const isExpanded = expandedAgents.has(agent.agentId);
|
||||
const isUpdating = updating === agent.agentId;
|
||||
const hasAny = agent.canCreateAgents || agent.grants.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -146,7 +169,7 @@ export function PermissionsPage(_props: PluginPageProps) {
|
||||
</span>
|
||||
<span>{agent.agentName}</span>
|
||||
</div>
|
||||
{agent.canCreateAgents && (
|
||||
{hasAny && (
|
||||
<span style={{
|
||||
fontSize: "0.75rem",
|
||||
backgroundColor: "#e5e7eb",
|
||||
@@ -154,7 +177,7 @@ export function PermissionsPage(_props: PluginPageProps) {
|
||||
padding: "0.125rem 0.5rem",
|
||||
borderRadius: "999px"
|
||||
}}>
|
||||
can hire
|
||||
{agent.grants.length + (agent.canCreateAgents ? 1 : 0)} permissions
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -163,17 +186,17 @@ export function PermissionsPage(_props: PluginPageProps) {
|
||||
<div style={{ padding: "0 1.25rem 1.25rem" }}>
|
||||
<fieldset style={{ border: "none", padding: 0, margin: 0 }}>
|
||||
<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
|
||||
htmlFor={`perm-${agent.agentId}-canCreateAgents`}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
cursor: isUpdating ? "wait" : "pointer",
|
||||
cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer",
|
||||
padding: "0.75rem",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: isUpdating ? "#f9fafb" : "#fafafa",
|
||||
backgroundColor: updating === `${agent.agentId}:legacy` ? "#f9fafb" : "#fafafa",
|
||||
border: "1px solid #e5e7eb",
|
||||
transition: "background-color 0.2s"
|
||||
}}
|
||||
@@ -182,22 +205,64 @@ export function PermissionsPage(_props: PluginPageProps) {
|
||||
id={`perm-${agent.agentId}-canCreateAgents`}
|
||||
type="checkbox"
|
||||
checked={agent.canCreateAgents}
|
||||
onChange={() => handleToggle(agent.agentId, agent.canCreateAgents)}
|
||||
onChange={() => handleToggleLegacy(agent.agentId, agent.canCreateAgents)}
|
||||
disabled={updating !== null}
|
||||
style={{ width: "18px", height: "18px", cursor: isUpdating ? "wait" : "pointer" }}
|
||||
style={{ width: "18px", height: "18px", cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer" }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500 }}>{PERMISSION_LABELS["canCreateAgents"]}</div>
|
||||
<div style={{ fontWeight: 500 }}>Can Hire Agents (legacy)</div>
|
||||
<div style={{ fontSize: "0.75rem", color: "#666" }}>
|
||||
{PERMISSION_DESCRIPTIONS["canCreateAgents"]}
|
||||
Allows this agent to create (hire) new agents. Also grants task assignment rights.
|
||||
</div>
|
||||
</div>
|
||||
{isUpdating && (
|
||||
{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={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
cursor: isUpdating ? "wait" : "pointer",
|
||||
padding: "0.75rem",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: isUpdating ? "#f9fafb" : "#fafafa",
|
||||
border: "1px solid #e5e7eb",
|
||||
transition: "background-color 0.2s"
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id={`perm-${agent.agentId}-${key}`}
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={() => handleToggleGrant(agent.agentId, key, enabled)}
|
||||
disabled={updating !== null}
|
||||
style={{ width: "18px", height: "18px", cursor: isUpdating ? "wait" : "pointer" }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500 }}>{PERMISSION_LABELS[key]}</div>
|
||||
<div style={{ fontSize: "0.75rem", color: "#666" }}>
|
||||
{PERMISSION_DESCRIPTIONS[key]}
|
||||
</div>
|
||||
</div>
|
||||
{isUpdating && (
|
||||
<span style={{ color: "#888", fontSize: "0.75rem" }} role="status">
|
||||
updating...
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -208,4 +273,4 @@ export function PermissionsPage(_props: PluginPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { usePluginData, usePluginAction, usePluginToast } 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 {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
canCreateAgents: boolean;
|
||||
grants: string[];
|
||||
}
|
||||
|
||||
const layoutStyle = {
|
||||
@@ -78,7 +79,8 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
|
||||
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 [expandedAgents, setExpandedAgents] = useState<Set<string>>(new Set());
|
||||
@@ -96,14 +98,15 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleToggle(agentId: string, currentEnabled: boolean) {
|
||||
setUpdating(agentId);
|
||||
async function handleToggleLegacy(agentId: string, currentEnabled: boolean) {
|
||||
if (!companyId) return;
|
||||
setUpdating(`${agentId}:legacy`);
|
||||
try {
|
||||
await togglePermission({ agentId, companyId, enabled: !currentEnabled });
|
||||
await toggleLegacy({ agentId, companyId, enabled: !currentEnabled });
|
||||
await refresh();
|
||||
toast({
|
||||
title: "Permission updated",
|
||||
body: `${PERMISSION_LABELS["canCreateAgents"]}: ${!currentEnabled ? "allowed" : "denied"}`,
|
||||
body: `Can Hire Agents: ${!currentEnabled ? "allowed" : "denied"}`,
|
||||
tone: "success",
|
||||
});
|
||||
} 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) {
|
||||
return (
|
||||
<div style={layoutStyle}>
|
||||
@@ -162,14 +192,14 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
|
||||
<div style={layoutStyle}>
|
||||
<div style={{ display: "grid", gap: "6px" }}>
|
||||
<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 style={{ display: "grid", gap: "10px" }}>
|
||||
{agentsData.map((agent) => {
|
||||
const isExpanded = expandedAgents.has(agent.agentId);
|
||||
const isUpdating = updating === agent.agentId;
|
||||
const hasAny = agent.canCreateAgents || agent.grants.length > 0;
|
||||
|
||||
return (
|
||||
<div key={agent.agentId} style={cardStyle}>
|
||||
@@ -178,9 +208,9 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
|
||||
<div style={{ display: "grid", gap: "2px", flex: 1 }}>
|
||||
<strong style={{ fontSize: "13px" }}>{agent.agentName}</strong>
|
||||
</div>
|
||||
{agent.canCreateAgents && (
|
||||
{hasAny && (
|
||||
<span style={{ ...mutedTextStyle, fontSize: "11px" }}>
|
||||
can hire
|
||||
{agent.grants.length + (agent.canCreateAgents ? 1 : 0)} permissions
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -190,27 +220,59 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<div style={permissionRowStyle}>
|
||||
<div style={{ display: "grid", gap: "2px", flex: 1 }}>
|
||||
<div style={{ fontSize: "12px", fontWeight: 500 }}>{PERMISSION_LABELS["canCreateAgents"]}</div>
|
||||
<div style={{ ...mutedTextStyle, fontSize: "11px" }}>{PERMISSION_DESCRIPTIONS["canCreateAgents"]}</div>
|
||||
<div style={{ fontSize: "12px", fontWeight: 500 }}>Can Hire Agents (legacy)</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" }}>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: "6px", cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agent.canCreateAgents}
|
||||
disabled={updating !== null}
|
||||
onChange={() => handleToggle(agent.agentId, agent.canCreateAgents)}
|
||||
onChange={() => handleToggleLegacy(agent.agentId, agent.canCreateAgents)}
|
||||
style={{
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
accentColor: "var(--foreground)",
|
||||
cursor: isUpdating ? "wait" : "pointer",
|
||||
cursor: updating === `${agent.agentId}:legacy` ? "wait" : "pointer",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: "11px", opacity: 0.8 }}>
|
||||
{isUpdating ? "saving…" : agent.canCreateAgents ? "Allowed" : "Denied"}
|
||||
{updating === `${agent.agentId}:legacy` ? "saving…" : agent.canCreateAgents ? "Allowed" : "Denied"}
|
||||
</span>
|
||||
</label>
|
||||
</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>
|
||||
) : null}
|
||||
@@ -220,4 +282,4 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,32 +5,30 @@ interface AgentPermissionsSummary {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
canCreateAgents: boolean;
|
||||
grants: string[];
|
||||
}
|
||||
|
||||
const plugin = definePlugin({
|
||||
async setup(ctx) {
|
||||
// Per-agent data for the detail tab
|
||||
ctx.data.register("agent-permissions", async (params) => {
|
||||
const agentId = typeof params.agentId === "string" ? params.agentId : "";
|
||||
const companyId = typeof params.companyId === "string" ? params.companyId : "";
|
||||
|
||||
if (!agentId) 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;
|
||||
if (!agentId || !companyId) return null;
|
||||
|
||||
const agent = await ctx.agents.get(agentId, companyId);
|
||||
if (!agent) return null;
|
||||
|
||||
const grants = await ctx.agents.getGrants(agentId, companyId);
|
||||
|
||||
return {
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
canCreateAgents: agent.permissions?.canCreateAgents ?? false,
|
||||
grants,
|
||||
};
|
||||
});
|
||||
|
||||
// All-agents list for the permissions page and settings page
|
||||
ctx.data.register("all-agents-permissions", async (params) => {
|
||||
const companyId = typeof params.companyId === "string" ? params.companyId : "";
|
||||
const limit = Math.min(
|
||||
@@ -39,20 +37,65 @@ const plugin = definePlugin({
|
||||
);
|
||||
const offset = Math.max(0, Number(params.offset) || 0);
|
||||
|
||||
const agents = companyId
|
||||
? await ctx.agents.list({ companyId, limit, offset })
|
||||
: [];
|
||||
if (!companyId) return [];
|
||||
|
||||
const result: AgentPermissionsSummary[] = agents.map(agent => ({
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
canCreateAgents: agent.permissions?.canCreateAgents ?? false,
|
||||
}));
|
||||
const agents = await ctx.agents.list({ companyId, limit, offset });
|
||||
|
||||
return result;
|
||||
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,
|
||||
agentName: agent.name,
|
||||
canCreateAgents: agent.permissions?.canCreateAgents ?? false,
|
||||
grants,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
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 {
|
||||
agentId: string;
|
||||
companyId: string;
|
||||
@@ -83,4 +126,4 @@ const plugin = definePlugin({
|
||||
});
|
||||
|
||||
export default plugin;
|
||||
runWorker(plugin, import.meta.url);
|
||||
runWorker(plugin, import.meta.url);
|
||||
Reference in New Issue
Block a user