# Agent Permissions Plugin - Implementation Plan **Status:** ✅ Complete (March 16, 2026) ## Overview This plugin provides per-agent permission toggling, allowing fine-grained control over what actions each agent can perform within Paperclip. ## Summary of Completed Work - ✅ Plugin scaffolded with manifest, worker, and UI entry points - ✅ Worker implements 3 handlers: `agent-permissions`, `all-agents-permissions`, `toggle-agent-permission` - ✅ UI components: `AgentPermissionsTab` (detail tab) and `PermissionsNav` (sidebar) - ✅ 6 permission keys implemented with proper validation - ✅ 18 unit tests passing - ✅ Build, typecheck all successful - ✅ Constants extracted to shared module for DRY code ## 6 Permission Keys | Key | Description | |-----|-------------| | `agents:create` | Create new agents (agent hiring) | | `users:invite` | Invite board users to the company | | `users:manage_permissions` | Manage user permissions | | `tasks:assign` | Assign tasks to agents | | `tasks:assign_scope` | Scope for task assignment (limits which agents can be assigned) | | `joins:approve` | Approve join requests | ## Current State - Only `canCreateAgents` (`agents:create`) is exposed on the agent permissions object - All 6 permissions can be granted to board users via the `principal_permission_grants` table - No per-agent permission granular control exists yet --- ## Architecture ### Plugin Structure ``` paperclip-plugins/plugin-agent-permissions/ ├── package.json ├── tsconfig.json ├── esbuild.config.mjs ├── README.md └── src/ ├── manifest.ts ├── worker.ts └── ui/ ├── index.tsx ├── AgentPermissionsTab.tsx └── PermissionToggle.tsx ``` ### Required Capabilities ```typescript capabilities: [ "agents.read", // Read agent data including permissions "ui.detailTab.register", // Add tab to agent detail page "ui.sidebar.register" // Add sidebar navigation item ] ``` --- ## Implementation Steps ### Step 1: Scaffold Plugin ```bash cd /home/mike/code/paperclip_plugins mkdir plugin-agent-permissions cd plugin-agent-permissions pnpm init pnpm add @paperclipai/plugin-sdk pnpm add -D typescript esbuild tsx ``` ### Step 2: Create Manifest ```typescript // src/manifest.ts import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk"; const manifest: PaperclipPluginManifestV1 = { id: "paperclipai.plugin-agent-permissions", apiVersion: 1, version: "0.1.0", displayName: "Agent Permissions", description: "Per-agent permission toggling for fine-grained access control", author: "FrenoCorp", categories: ["permissions", "agent-management"], capabilities: [ "agents.read", "ui.detailTab.register", "ui.sidebar.register" ], entrypoints: { worker: "./dist/worker.js", ui: "./dist/ui" }, ui: { slots: [ { type: "detailTab", id: "permissions", displayName: "Permissions", exportName: "AgentPermissionsTab", entityTypes: ["agent"] }, { type: "sidebar", id: "permissions-nav", displayName: "Permissions", exportName: "PermissionsNav" } ] } }; export default manifest; ``` ### Step 3: Implement Worker ```typescript // src/worker.ts import { definePlugin, runWorker } from "@paperclipai/plugin-sdk"; const PERMISSION_KEYS = [ "agents:create", "users:invite", "users:manage_permissions", "tasks:assign", "tasks:assign_scope", "joins:approve" ] as const; type PermissionKey = (typeof PERMISSION_KEYS)[number]; interface AgentPermissions { agentId: string; permissions: Record; } const plugin = definePlugin({ async setup(ctx) { // Data handler: Get permissions for a specific agent ctx.data.register("agent-permissions", async (params) => { const { agentId } = params as { agentId: string }; // Query principal_permission_grants for this agent const grants = await ctx.entities.query(` SELECT ppg.permission_key FROM principal_permission_grants ppg JOIN principals p ON ppg.principal_id = p.id WHERE p.type = 'agent' AND p.external_id = $1 `, [agentId]); const grantedPermissions = new Set(grants.map((g: any) => g.permission_key)); return { agentId, permissions: PERMISSION_KEYS.reduce((acc, key) => ({ ...acc, [key]: grantedPermissions.has(key) }), {} as Record) }; }); // Action: Toggle a permission for an agent ctx.actions.register("toggle-agent-permission", async (params) => { const { agentId, permissionKey, enabled } = params as { agentId: string; permissionKey: PermissionKey; enabled: boolean; }; // Find or create principal for agent let principalId = await ctx.entities.query(` SELECT id FROM principals WHERE type = 'agent' AND external_id = $1 `, [agentId]); if (!principalId || principalId.length === 0) { // Create principal if it doesn't exist const result = await ctx.entities.create("principals", { type: "agent", external_id: agentId }); principalId = result.id; } if (enabled) { // Grant permission await ctx.entities.create("principal_permission_grants", { principal_id: principalId, permission_key: permissionKey }); } else { // Revoke permission await ctx.entities.query(` DELETE FROM principal_permission_grants WHERE principal_id = $1 AND permission_key = $2 `, [principalId, permissionKey]); } return { success: true }; }); // Data handler: Get all agents with their permissions ctx.data.register("all-agents-permissions", async () => { const agents = await ctx.agents.list(); const result = await Promise.all(agents.map(async (agent) => { const grants = await ctx.entities.query(` SELECT permission_key FROM principal_permission_grants WHERE principal_id IN ( SELECT id FROM principals WHERE type = 'agent' AND external_id = $1 ) `, [agent.id]); const grantedPermissions = new Set(grants.map((g: any) => g.permission_key)); return { agentId: agent.id, agentName: agent.name, permissions: PERMISSION_KEYS.reduce((acc, key) => ({ ...acc, [key]: grantedPermissions.has(key) }), {} as Record) }; })); return result; }); }, async onHealth() { return { status: "ok", message: "Agent permissions plugin running" }; } }); export default plugin; runWorker(plugin, import.meta.url); ``` ### Step 4: Implement UI Components ```typescript // src/ui/index.tsx export { AgentPermissionsTab } from './AgentPermissionsTab'; export { PermissionsNav } from './PermissionsNav'; // src/ui/AgentPermissionsTab.tsx import { useState } from "react"; import { useHostContext, usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui"; import type { PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui"; const PERMISSION_LABELS: Record = { "agents:create": "Create Agents", "users:invite": "Invite Users", "users:manage_permissions": "Manage Permissions", "tasks:assign": "Assign Tasks", "tasks:assign_scope": "Task Assignment Scope", "joins:approve": "Approve Join Requests" }; export function AgentPermissionsTab({ context }: PluginDetailTabProps) { const { entityId: agentId } = context; const { data, refresh } = usePluginData<{ agentId: string; permissions: Record; }>("agent-permissions", { agentId }); const togglePermission = usePluginAction("toggle-agent-permission"); const [updating, setUpdating] = useState(null); if (!data) { return
Loading permissions...
; } async function handleToggle(permissionKey: string, enabled: boolean) { setUpdating(permissionKey); try { await togglePermission({ agentId, permissionKey, enabled: !enabled }); await refresh(); } catch (error) { console.error("Failed to toggle permission:", error); } finally { setUpdating(null); } } return (

Agent Permissions

Control what actions this agent can perform.

{Object.entries(data.permissions).map(([key, enabled]) => ( ))}
{updating && (

Updating...

)}
); } ``` ### Step 5: Build Configuration ```javascript // esbuild.config.mjs import { createPluginBundlerPresets } from "@paperclipai/plugin-sdk/bundlers"; import { build } from "esbuild"; const presets = createPluginBundlerPresets({ uiEntry: "src/ui/index.tsx" }); // Build worker await build(presets.esbuild.worker); // Build manifest await build(presets.esbuild.manifest); // Build UI await build(presets.esbuild.ui); ``` --- ## Testing ### Unit Tests ```typescript // tests/worker.test.ts import { createTestHarness } from "@paperclipai/plugin-sdk/testing"; import plugin from "../src/worker"; import manifest from "../src/manifest"; const harness = createTestHarness({ manifest }); await plugin.definition.setup(harness.ctx); // Test getting agent permissions const permissions = await harness.callData("agent-permissions", { agentId: "test-agent-123" }); console.log(permissions); // Test toggling permission await harness.callAction("toggle-agent-permission", { agentId: "test-agent-123", permissionKey: "agents:create", enabled: true }); ``` --- ## Installation ```bash # In your Paperclip instance cd /path/to/paperclip pnpm add file:/path/to/paperclip-plugins/plugin-agent-permissions ``` Or configure in the plugin settings UI with the local path. --- ## Notes 1. **Database Schema**: The `principal_permission_grants` table links principals (agents, users) to their permissions 2. **Principal Types**: Agents have `type = 'agent'`, board users have `type = 'user'` 3. **External ID**: Agents are referenced by their UUID in the `external_id` column of the `principals` table 4. **Real-time Updates**: UI components should call `refresh()` after permission changes to update the display