Files
paperclip-plugins/plugin-agent-permissions/IMPLEMENTATION_PLAN.md

392 lines
11 KiB
Markdown

# 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<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
```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<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
```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