From 33768b748cbd6469c9d12c5a89ae6e2b2e22d932 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sun, 15 Mar 2026 19:08:14 -0400 Subject: [PATCH] FRE-339: Add implementation plan for agent permissions plugin --- .../IMPLEMENTATION_PLAN.md | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 plugin-agent-permissions/IMPLEMENTATION_PLAN.md diff --git a/plugin-agent-permissions/IMPLEMENTATION_PLAN.md b/plugin-agent-permissions/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..a59ae5a --- /dev/null +++ b/plugin-agent-permissions/IMPLEMENTATION_PLAN.md @@ -0,0 +1,391 @@ +# Agent Permissions Plugin - Implementation Plan + +## Overview + +This plugin provides per-agent permission toggling, allowing fine-grained control over what actions each agent can perform within Paperclip. + +## 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