Files
paperclip-plugins/plugin-agent-permissions/IMPLEMENTATION_PLAN.md
2026-03-16 16:48:38 -04:00

11 KiB

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

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

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

// 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

// 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<PermissionKey, boolean>;
}

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<PermissionKey, boolean>)
      };
    });

    // 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<PermissionKey, boolean>)
        };
      }));
      
      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

// 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<string, string> = {
  "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<string, boolean>;
  }>("agent-permissions", { agentId });
  
  const togglePermission = usePluginAction("toggle-agent-permission");
  const [updating, setUpdating] = useState<string | null>(null);

  if (!data) {
    return <div>Loading permissions...</div>;
  }

  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 (
    <div style={{ padding: "1rem", maxWidth: "600px" }}>
      <h2 style={{ marginBottom: "1.5rem" }}>Agent Permissions</h2>
      <p style={{ color: "#666", marginBottom: "1.5rem" }}>
        Control what actions this agent can perform.
      </p>
      
      <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
        {Object.entries(data.permissions).map(([key, enabled]) => (
          <label key={key} style={{ 
            display: "flex", 
            alignItems: "center",
            gap: "0.5rem",
            cursor: updating === key ? "wait" : "pointer"
          }}>
            <input
              type="checkbox"
              checked={enabled}
              onChange={() => handleToggle(key, enabled)}
              disabled={updating !== null}
              style={{ width: "18px", height: "18px" }}
            />
            <span style={{ fontWeight: 500 }}>{PERMISSION_LABELS[key] || key}</span>
          </label>
        ))}
      </div>

      {updating && (
        <p style={{ color: "#666", fontSize: "0.875rem", marginTop: "1rem" }}>
          Updating...
        </p>
      )}
    </div>
  );
}

Step 5: Build Configuration

// 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

// 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

# 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