From ecd78f7528205af83f4598ce43372190e18c0522 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 17 Mar 2026 14:33:32 -0400 Subject: [PATCH] plugin side confirmed --- plugin-agent-permissions/src/constants.ts | 24 ++-- plugin-agent-permissions/src/manifest.ts | 2 + .../src/ui/AgentPermissionsTab.tsx | 128 +++++++++++++----- .../src/ui/PermissionsPage.tsx | 105 +++++++++++--- .../src/ui/PermissionsSettingsPage.tsx | 96 ++++++++++--- plugin-agent-permissions/src/worker.ts | 81 ++++++++--- 6 files changed, 336 insertions(+), 100 deletions(-) diff --git a/plugin-agent-permissions/src/constants.ts b/plugin-agent-permissions/src/constants.ts index 067297a..10fd2d8 100644 --- a/plugin-agent-permissions/src/constants.ts +++ b/plugin-agent-permissions/src/constants.ts @@ -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 = { - "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 = { - "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; \ No newline at end of file diff --git a/plugin-agent-permissions/src/manifest.ts b/plugin-agent-permissions/src/manifest.ts index b728134..e782a55 100644 --- a/plugin-agent-permissions/src/manifest.ts +++ b/plugin-agent-permissions/src/manifest.ts @@ -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" diff --git a/plugin-agent-permissions/src/ui/AgentPermissionsTab.tsx b/plugin-agent-permissions/src/ui/AgentPermissionsTab.tsx index 8b9d89c..e94c16a 100644 --- a/plugin-agent-permissions/src/ui/AgentPermissionsTab.tsx +++ b/plugin-agent-permissions/src/ui/AgentPermissionsTab.tsx @@ -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(null); const [lastError, setLastError] = useState(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
Loading permissions...
; if (error) return (
@@ -29,21 +67,6 @@ export function AgentPermissionsTab({ context }: PluginDetailTabProps) { ); if (!data) return
No permissions data available
; - 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 (

Agent Permissions

@@ -72,45 +95,80 @@ export function AgentPermissionsTab({ context }: PluginDetailTabProps) { Permission toggles
+ + {PERMISSION_KEYS.map((key) => { + const enabled = hasGrant(key); + return ( + + ); + })}
); -} +} \ No newline at end of file diff --git a/plugin-agent-permissions/src/ui/PermissionsPage.tsx b/plugin-agent-permissions/src/ui/PermissionsPage.tsx index f9d09aa..5e86d83 100644 --- a/plugin-agent-permissions/src/ui/PermissionsPage.tsx +++ b/plugin-agent-permissions/src/ui/PermissionsPage.tsx @@ -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(null); const [lastError, setLastError] = useState(null); const [expandedAgents, setExpandedAgents] = useState>(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 (
@@ -105,7 +128,7 @@ export function PermissionsPage(_props: PluginPageProps) {
{agentsData.map(agent => { const isExpanded = expandedAgents.has(agent.agentId); - const isUpdating = updating === agent.agentId; + const hasAny = agent.canCreateAgents || agent.grants.length > 0; return (
{agent.agentName}
- {agent.canCreateAgents && ( + {hasAny && ( - can hire + {agent.grants.length + (agent.canCreateAgents ? 1 : 0)} permissions )} @@ -163,17 +186,17 @@ export function PermissionsPage(_props: PluginPageProps) {
Permission toggles for {agent.agentName} -
+
+ + {PERMISSION_KEYS.map((key) => { + const enabled = hasGrant(agent, key); + const isUpdating = updating === `${agent.agentId}:${key}`; + return ( + + ); + })}
@@ -208,4 +273,4 @@ export function PermissionsPage(_props: PluginPageProps) {
); -} +} \ No newline at end of file diff --git a/plugin-agent-permissions/src/ui/PermissionsSettingsPage.tsx b/plugin-agent-permissions/src/ui/PermissionsSettingsPage.tsx index c4757f0..28c5bb3 100644 --- a/plugin-agent-permissions/src/ui/PermissionsSettingsPage.tsx +++ b/plugin-agent-permissions/src/ui/PermissionsSettingsPage.tsx @@ -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>(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 (
@@ -162,14 +192,14 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
- 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.
{agentsData.map((agent) => { const isExpanded = expandedAgents.has(agent.agentId); - const isUpdating = updating === agent.agentId; + const hasAny = agent.canCreateAgents || agent.grants.length > 0; return (
@@ -178,9 +208,9 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
{agent.agentName}
- {agent.canCreateAgents && ( + {hasAny && ( - can hire + {agent.grants.length + (agent.canCreateAgents ? 1 : 0)} permissions )}
@@ -190,27 +220,59 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
-
{PERMISSION_LABELS["canCreateAgents"]}
-
{PERMISSION_DESCRIPTIONS["canCreateAgents"]}
+
Can Hire Agents (legacy)
+
+ Allows this agent to create (hire) new agents. Also grants task assignment rights. +
-
+ + {PERMISSION_KEYS.map((key) => { + const enabled = hasGrant(agent, key); + const isKeyUpdating = updating === `${agent.agentId}:${key}`; + return ( +
+
+
{PERMISSION_LABELS[key]}
+
{PERMISSION_DESCRIPTIONS[key]}
+
+ +
+ ); + })}
) : null} @@ -220,4 +282,4 @@ export function PermissionsSettingsPage({ context }: PluginSettingsPageProps) {
); -} +} \ No newline at end of file diff --git a/plugin-agent-permissions/src/worker.ts b/plugin-agent-permissions/src/worker.ts index 6d1943a..71760e9 100644 --- a/plugin-agent-permissions/src/worker.ts +++ b/plugin-agent-permissions/src/worker.ts @@ -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); \ No newline at end of file