diff --git a/Dockerfile b/Dockerfile
index 3ee0fb81282..03f26942b40 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -34,7 +34,6 @@ COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-lo
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
COPY packages/plugins/sdk/package.json packages/plugins/sdk/
COPY --parents packages/plugins/sandbox-providers/./*/package.json packages/plugins/sandbox-providers/
-COPY packages/plugins/paperclip-ee-permissions/package.json packages/plugins/paperclip-ee-permissions/
COPY packages/plugins/paperclip-plugin-fake-sandbox/package.json packages/plugins/paperclip-plugin-fake-sandbox/
COPY packages/plugins/plugin-llm-wiki/package.json packages/plugins/plugin-llm-wiki/
COPY packages/plugins/plugin-workspace-diff/package.json packages/plugins/plugin-workspace-diff/
diff --git a/packages/plugins/paperclip-ee-permissions/.gitignore b/packages/plugins/paperclip-ee-permissions/.gitignore
deleted file mode 100644
index 84f3869a736..00000000000
--- a/packages/plugins/paperclip-ee-permissions/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-dist
-node_modules
-*.tsbuildinfo
diff --git a/packages/plugins/paperclip-ee-permissions/README.md b/packages/plugins/paperclip-ee-permissions/README.md
deleted file mode 100644
index f3bfe5d700f..00000000000
--- a/packages/plugins/paperclip-ee-permissions/README.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# @paperclipai/plugin-paperclip-ee-permissions
-
-Closed-source Paperclip EE plugin that hosts the advanced company access,
-grants, and authorization-policy UX.
-
-## Status
-
-Phase 5 implementation surface. The plugin registers a `companySettingsPage`
-slot at `/permissions`, activates a per-company EE mode stub, and renders the
-advanced authorization controls through typed plugin-SDK access and
-authorization clients.
-
-## Capabilities
-
-- `access.members.read` / `access.members.write`
-- `access.invites.read` / `access.invites.write`
-- `authorization.grants.read` / `authorization.grants.write`
-- `authorization.policies.read` / `authorization.policies.write`
-- `authorization.audit.read`
-- `companies.read`, `agents.read`, `issues.read`
-- `plugin.state.read`, `plugin.state.write`
-- `instance.settings.register`, `ui.page.register`
-
-The plugin never imports from `ui/src/api/access.ts` or any other host UI
-internal — all data flows through `ctx.access.*` and `ctx.authorization.*`.
-
-## Deterministic missing-state UX
-
-The page renders one of three states:
-
-| Condition | UI |
-| ------------------------------------------ | ----------------------------------------------------- |
-| `companyId === null` | "No active company" card |
-| License inactive for company | "Advanced permissions mode is not active" with Activate button |
-| License active, host returns capability or worker error | Inline warning card; rest of the page still renders |
-
-## Advanced surfaces
-
-When licensed, the company settings page exposes:
-
-- Agent visibility policy editing (`discoverable` / `private`).
-- Assignment policy editing with broad, scoped-to-agent, and clear grant modes.
-- Protected-agent approval requirement fields.
-- Permission preview and explanation panels that call the same core
- authorization decision path used by enforcement.
-- Authorization audit filters for actor, resource, action, and decision.
-
-The license check is a per-company plugin-state stub today; later phases
-replace it with a core-owned `advanced_permissions_enabled` flag.
-
-## Scripts
-
-- `pnpm build` — bundle worker, manifest, and UI via esbuild
-- `pnpm test` — run vitest harness tests for the worker
-
-## Layout
-
-```
-src/
- manifest.ts # Slot + capability declarations
- worker.ts # getData / performAction handlers
- ui/
- index.tsx # Re-exports the slot component
- app.tsx # EePermissionsCompanySettingsPage
-tests/
- plugin.spec.ts # Worker harness tests
-```
diff --git a/packages/plugins/paperclip-ee-permissions/esbuild.config.mjs b/packages/plugins/paperclip-ee-permissions/esbuild.config.mjs
deleted file mode 100644
index b5cfd36ed95..00000000000
--- a/packages/plugins/paperclip-ee-permissions/esbuild.config.mjs
+++ /dev/null
@@ -1,17 +0,0 @@
-import esbuild from "esbuild";
-import { createPluginBundlerPresets } from "@paperclipai/plugin-sdk/bundlers";
-
-const presets = createPluginBundlerPresets({ uiEntry: "src/ui/index.tsx" });
-const watch = process.argv.includes("--watch");
-
-const workerCtx = await esbuild.context(presets.esbuild.worker);
-const manifestCtx = await esbuild.context(presets.esbuild.manifest);
-const uiCtx = await esbuild.context(presets.esbuild.ui);
-
-if (watch) {
- await Promise.all([workerCtx.watch(), manifestCtx.watch(), uiCtx.watch()]);
- console.log("esbuild watch mode enabled for worker, manifest, and ui");
-} else {
- await Promise.all([workerCtx.rebuild(), manifestCtx.rebuild(), uiCtx.rebuild()]);
- await Promise.all([workerCtx.dispose(), manifestCtx.dispose(), uiCtx.dispose()]);
-}
diff --git a/packages/plugins/paperclip-ee-permissions/package.json b/packages/plugins/paperclip-ee-permissions/package.json
deleted file mode 100644
index 29d7b4ff7f2..00000000000
--- a/packages/plugins/paperclip-ee-permissions/package.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "name": "@paperclipai/plugin-paperclip-ee-permissions",
- "version": "0.1.0",
- "type": "module",
- "private": true,
- "description": "Paperclip EE plugin that hosts advanced company access, grants, and authorization-policy UX.",
- "scripts": {
- "prebuild": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps",
- "build": "node ./esbuild.config.mjs",
- "build:rollup": "rollup -c",
- "dev": "node ./esbuild.config.mjs --watch",
- "test": "vitest run --config ./vitest.config.ts",
- "typecheck": "pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && tsc --noEmit"
- },
- "paperclipPlugin": {
- "manifest": "./dist/manifest.js",
- "worker": "./dist/worker.js",
- "ui": "./dist/ui/"
- },
- "keywords": [
- "paperclip",
- "plugin",
- "ee",
- "permissions",
- "authorization"
- ],
- "author": "Paperclip",
- "license": "UNLICENSED",
- "devDependencies": {
- "@paperclipai/plugin-sdk": "workspace:*",
- "@paperclipai/shared": "workspace:*",
- "@rollup/plugin-node-resolve": "^16.0.1",
- "@rollup/plugin-typescript": "^12.1.2",
- "@types/node": "^24.6.0",
- "@types/react": "^19.0.8",
- "esbuild": "^0.27.3",
- "rollup": "^4.38.0",
- "tslib": "^2.8.1",
- "typescript": "^5.7.3",
- "vitest": "^3.0.5"
- },
- "peerDependencies": {
- "react": ">=18"
- }
-}
diff --git a/packages/plugins/paperclip-ee-permissions/rollup.config.mjs b/packages/plugins/paperclip-ee-permissions/rollup.config.mjs
deleted file mode 100644
index ccee40a7443..00000000000
--- a/packages/plugins/paperclip-ee-permissions/rollup.config.mjs
+++ /dev/null
@@ -1,28 +0,0 @@
-import { nodeResolve } from "@rollup/plugin-node-resolve";
-import typescript from "@rollup/plugin-typescript";
-import { createPluginBundlerPresets } from "@paperclipai/plugin-sdk/bundlers";
-
-const presets = createPluginBundlerPresets({ uiEntry: "src/ui/index.tsx" });
-
-function withPlugins(config) {
- if (!config) return null;
- return {
- ...config,
- plugins: [
- nodeResolve({
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
- }),
- typescript({
- tsconfig: "./tsconfig.json",
- declaration: false,
- declarationMap: false,
- }),
- ],
- };
-}
-
-export default [
- withPlugins(presets.rollup.manifest),
- withPlugins(presets.rollup.worker),
- withPlugins(presets.rollup.ui),
-].filter(Boolean);
diff --git a/packages/plugins/paperclip-ee-permissions/src/manifest.ts b/packages/plugins/paperclip-ee-permissions/src/manifest.ts
deleted file mode 100644
index e5ac1898040..00000000000
--- a/packages/plugins/paperclip-ee-permissions/src/manifest.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
-
-export const PLUGIN_ID = "paperclipai.plugin-paperclip-ee-permissions";
-
-export const SLOT_IDS = {
- companySettingsPage: "ee-permissions-company-settings-page",
-} as const;
-
-export const EXPORT_NAMES = {
- companySettingsPage: "EePermissionsCompanySettingsPage",
-} as const;
-
-const manifest: PaperclipPluginManifestV1 = {
- id: PLUGIN_ID,
- apiVersion: 1,
- version: "0.1.0",
- displayName: "Paperclip EE Permissions",
- description:
- "Advanced company access, grants, and authorization-policy UX for licensed Paperclip EE deployments.",
- author: "Paperclip",
- categories: ["ui"],
- capabilities: [
- "companies.read",
- "agents.read",
- "issues.read",
- "access.members.read",
- "access.invites.read",
- "authorization.grants.read",
- "authorization.policies.read",
- "authorization.audit.read",
- "access.members.write",
- "access.invites.write",
- "authorization.grants.write",
- "authorization.policies.write",
- "plugin.state.read",
- "plugin.state.write",
- "instance.settings.register",
- "ui.page.register",
- ],
- entrypoints: {
- worker: "./dist/worker.js",
- ui: "./dist/ui",
- },
- ui: {
- slots: [
- {
- type: "companySettingsPage",
- id: SLOT_IDS.companySettingsPage,
- displayName: "Permissions",
- exportName: EXPORT_NAMES.companySettingsPage,
- routePath: "permissions",
- },
- ],
- },
-};
-
-export default manifest;
diff --git a/packages/plugins/paperclip-ee-permissions/src/ui/app.tsx b/packages/plugins/paperclip-ee-permissions/src/ui/app.tsx
deleted file mode 100644
index 08b20ecf106..00000000000
--- a/packages/plugins/paperclip-ee-permissions/src/ui/app.tsx
+++ /dev/null
@@ -1,1227 +0,0 @@
-import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from "react";
-import {
- AssigneePicker,
- DataTable,
- ErrorBoundary,
- JsonTree,
- KeyValueList,
- MetricCard,
- ProjectPicker,
- Spinner,
- StatusBadge,
- useHostContext,
- usePluginAction,
- usePluginData,
- type DataTableColumn,
- type PluginCompanySettingsPageProps,
- type StatusBadgeVariant,
-} from "@paperclipai/plugin-sdk/ui";
-
-type HumanCompanyMembershipRole = "owner" | "admin" | "operator" | "viewer";
-type MemberEditableStatus = "pending" | "active" | "suspended";
-type MemberPrincipalType = "user" | "agent";
-type PermissionKey =
- | "agents:create"
- | "environments:manage"
- | "users:invite"
- | "users:manage_permissions"
- | "tasks:assign"
- | "tasks:assign_scope"
- | "tasks:manage_active_checkouts"
- | "joins:approve";
-
-const HUMAN_ROLE_LABELS: Record = {
- owner: "Owner",
- admin: "Admin",
- operator: "Operator",
- viewer: "Viewer",
-};
-
-const MEMBER_PERMISSION_KEYS: ReadonlyArray = [
- "agents:create",
- "users:invite",
- "users:manage_permissions",
- "tasks:assign",
- "tasks:manage_active_checkouts",
- "joins:approve",
- "environments:manage",
-];
-
-const PERMISSION_LABELS: Record = {
- "agents:create": "Create agents",
- "users:invite": "Invite humans and agents",
- "users:manage_permissions": "Manage members and grants",
- "tasks:assign": "Assign tasks",
- "tasks:assign_scope": "Assign scoped tasks",
- "tasks:manage_active_checkouts": "Manage active task checkouts",
- "joins:approve": "Approve join requests",
- "environments:manage": "Manage environments",
-};
-
-const IMPLICIT_ROLE_GRANTS: Record = {
- owner: ["agents:create", "users:invite", "users:manage_permissions", "tasks:assign", "joins:approve"],
- admin: ["agents:create", "users:invite", "tasks:assign", "joins:approve"],
- operator: ["tasks:assign"],
- viewer: [],
-};
-
-type MemberRecord = {
- id: string;
- companyId: string;
- principalType: MemberPrincipalType;
- principalId: string;
- status: MemberEditableStatus | "archived";
- membershipRole: HumanCompanyMembershipRole | string | null;
- grants: Array<{ permissionKey: string; scope: Record | null }>;
-};
-
-type AgentRecord = {
- id: string;
- name: string;
- role?: string | null;
- title?: string | null;
- status?: string | null;
-};
-
-type MemberAccessData = {
- companyId: string;
- warnings: Array<{ code: string; message: string }>;
- members: MemberRecord[];
- agents?: AgentRecord[];
-};
-
-type LicenseState = {
- status: "active" | "inactive";
- activatedAt?: string;
- activatedByUserId?: string | null;
- note?: string | null;
-};
-
-type PolicySummary = {
- companyId: string;
- permissionsMode: "simple";
- memberCount: number;
- activeMemberCount: number;
- grantCount: number;
- advancedPolicyAvailable: false;
-};
-
-type IssueRecord = {
- id: string;
- title: string;
- status: string;
- projectId?: string | null;
-};
-
-type DecisionRecord = {
- allowed: boolean;
- action: string;
- explanation: string;
- reason: string;
- grant?: {
- permissionKey: string;
- scope: Record | null;
- };
-};
-
-type Overview = {
- companyId: string;
- license: LicenseState;
- policySummary: PolicySummary | null;
- warnings: Array<{ code: string; message: string }>;
-};
-
-type AuditEntry = {
- id: string;
- actorType: string;
- actorId: string;
- action: string;
- entityType: string;
- entityId: string;
- details: Record | null;
- createdAt: string;
-};
-
-type AdvancedPolicyData = {
- companyId: string;
- summary: PolicySummary | null;
- warnings: Overview["warnings"];
- agents: AgentRecord[];
- issues: IssueRecord[];
- selected: {
- actorAgentId: string | null;
- targetAgentId: string | null;
- projectId: string | null;
- issueId: string | null;
- };
- agentPolicy: {
- resourceType: string;
- resourceId: string;
- policy: Record | null;
- updatedAt: string | null;
- } | null;
- actorGrants: Array<{
- permissionKey: string;
- scope: Record | null;
- }>;
- preview: DecisionRecord | null;
- explanation: DecisionRecord | null;
- auditEntries: AuditEntry[];
-};
-
-const layoutStack: CSSProperties = {
- display: "grid",
- gap: "16px",
- padding: "24px 0",
-};
-
-const cardStyle: CSSProperties = {
- border: "1px solid var(--border, #e2e8f0)",
- borderRadius: "8px",
- padding: "16px",
- display: "grid",
- gap: "12px",
- background: "var(--card, #ffffff)",
-};
-
-const subtleCardStyle: CSSProperties = {
- border: "1px solid var(--border, #e2e8f0)",
- borderRadius: "8px",
- padding: "12px",
- display: "grid",
- gap: "8px",
- background: "var(--background, transparent)",
-};
-
-const mutedTextStyle: CSSProperties = {
- color: "var(--muted-foreground, #64748b)",
- fontSize: "0.9rem",
- lineHeight: 1.5,
-};
-
-const sectionHeadingStyle: CSSProperties = {
- fontSize: "0.75rem",
- letterSpacing: "0.04em",
- textTransform: "uppercase",
- color: "var(--muted-foreground, #64748b)",
-};
-
-const rowStyle: CSSProperties = {
- display: "flex",
- alignItems: "center",
- flexWrap: "wrap",
- gap: "8px",
-};
-
-const gridStyle: CSSProperties = {
- display: "grid",
- gap: "12px",
- gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
-};
-
-const inputStyle: CSSProperties = {
- width: "100%",
- border: "1px solid var(--border, #cbd5e1)",
- borderRadius: "6px",
- padding: "8px 10px",
- background: "var(--background, transparent)",
- color: "inherit",
- fontSize: "0.85rem",
-};
-
-const buttonStyle: CSSProperties = {
- padding: "7px 12px",
- borderRadius: "6px",
- border: "1px solid var(--border, #cbd5e1)",
- background: "var(--background, transparent)",
- color: "inherit",
- cursor: "pointer",
-};
-
-const primaryButtonStyle: CSSProperties = {
- ...buttonStyle,
- background: "var(--foreground, #0f172a)",
- color: "var(--background, #ffffff)",
-};
-
-const warningStyle: CSSProperties = {
- border: "1px solid var(--warning-border, #facc15)",
- background: "var(--warning-muted, #fefce8)",
- borderRadius: "8px",
- padding: "10px 12px",
- color: "var(--warning-foreground, #713f12)",
-};
-
-const fieldStyle: CSSProperties = {
- display: "grid",
- gap: "6px",
-};
-
-function getErrorMessage(error: unknown): string {
- return error instanceof Error ? error.message : String(error);
-}
-
-function statusForDecision(allowed: boolean): StatusBadgeVariant {
- return allowed ? "ok" : "error";
-}
-
-function decisionLabel(allowed: boolean): string {
- return allowed ? "✓ Allowed" : "✕ Denied";
-}
-
-function membershipStatusVariant(status: string): StatusBadgeVariant {
- if (status === "active") return "ok";
- if (status === "pending") return "pending";
- if (status === "suspended" || status === "archived") return "warning";
- return "info";
-}
-
-function formatPermission(permissionKey: string): string {
- return PERMISSION_LABELS[permissionKey as PermissionKey] ?? permissionKey;
-}
-
-function formatScope(scope: Record | null | undefined): string {
- if (!scope || Object.keys(scope).length === 0) return "Any scope";
- const parts = Object.entries(scope).map(([key, value]) => `${key}: ${String(value)}`);
- return parts.join(", ");
-}
-
-function formatMode(value: string | null | undefined): string {
- if (!value) return "Not set";
- return value.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
-}
-
-function formatDate(value: string): string {
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return date.toLocaleString();
-}
-
-function getPolicySection(policy: Record | null | undefined, key: string): Record {
- const value = policy?.[key];
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {};
-}
-
-function getPolicyString(policy: Record | null | undefined, section: string, key: string, fallback: string) {
- const value = getPolicySection(policy, section)[key];
- return typeof value === "string" ? value : fallback;
-}
-
-function getPolicyBoolean(policy: Record | null | undefined, section: string, key: string, fallback: boolean) {
- const value = getPolicySection(policy, section)[key];
- return typeof value === "boolean" ? value : fallback;
-}
-
-function RawDisclosure({ label = "Raw response", data }: { label?: string; data: unknown }) {
- return (
-
- {label}
-
-
- );
-}
-
-function CapabilityWarning({ warnings }: { warnings: Overview["warnings"] }) {
- if (warnings.length === 0) return null;
- const denied = warnings.some((warning) => warning.code === "CAPABILITY_DENIED");
- return (
-
-
{denied ? "Some advanced data is unavailable." : "Some advanced data could not be loaded."}
-
- {denied
- ? "The plugin is missing one or more capability grants. Install the latest version or re-activate the plugin to restore the missing surfaces."
- : "Existing restrictions remain enforced by core. Retry once the underlying service is reachable."}
-
-
- {warnings.map((warning, index) => (
-
- {warning.code}: {warning.message}
-
- ))}
-
-
- );
-}
-
-function LoadingState({ label }: { label: string }) {
- return (
-
-
- {label}
-
- );
-}
-
-function MissingCompanyState() {
- return (
-
-
-
Permissions
-
No active company
-
Switch into a company to manage advanced permissions.
-
-
- );
-}
-
-function UnlicensedState({
- companyId,
- onActivate,
- activating,
-}: {
- companyId: string;
- onActivate: () => void;
- activating: boolean;
-}) {
- return (
-
-
-
Paperclip EE Permissions
-
Advanced permissions mode is not active
-
- Members can collaborate across this company by default. Activate Paperclip EE permissions to unlock scoped grants, protected-agent controls, assignment previews, and audit filters.
-
-
-
- {activating ? : "Activate for this company"}
-
-
-
{companyId} }]} />
-
-
- );
-}
-
-type PrincipalProfile = {
- label: string;
- secondary: string;
-};
-
-function profileForPrincipal(member: MemberRecord, agents: AgentRecord[]): PrincipalProfile {
- if (member.principalType === "agent") {
- const agent = agents.find((entry) => entry.id === member.principalId);
- if (agent) {
- return {
- label: agent.name,
- secondary: [agent.title || agent.role, agent.status, agent.id].filter(Boolean).join(" / "),
- };
- }
- return {
- label: "Agent",
- secondary: member.principalId,
- };
- }
- if (member.principalId.includes("@")) {
- const [localPart] = member.principalId.split("@");
- return {
- label: localPart.split(/[._-]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || member.principalId,
- secondary: member.principalId,
- };
- }
- return {
- label: "Board user",
- secondary: member.principalId,
- };
-}
-
-type MemberTableRow = {
- id: string;
- member: MemberRecord;
- principal: ReactNode;
- role: ReactNode;
- status: ReactNode;
- grants: ReactNode;
- action: ReactNode;
-};
-
-function MembersPanel({ companyId }: { companyId: string }) {
- const query = usePluginData("memberAccess", { companyId });
- const saveMemberAccess = usePluginAction("saveMemberAccess");
- const [editingMemberId, setEditingMemberId] = useState(null);
- const [draftRole, setDraftRole] = useState("");
- const [draftStatus, setDraftStatus] = useState("active");
- const [draftGrants, setDraftGrants] = useState>(new Set());
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState(null);
-
- const members = query.data?.members ?? [];
- const agents = query.data?.agents ?? [];
- const editingMember = useMemo(
- () => members.find((member) => member.id === editingMemberId) ?? null,
- [members, editingMemberId],
- );
- const editingProfile = editingMember ? profileForPrincipal(editingMember, agents) : null;
- const implicitGrantKeys = useMemo(
- () => (draftRole ? IMPLICIT_ROLE_GRANTS[draftRole] : []),
- [draftRole],
- );
-
- useEffect(() => {
- if (!editingMember) return;
- const role = editingMember.membershipRole;
- setDraftRole((role && role in HUMAN_ROLE_LABELS ? (role as HumanCompanyMembershipRole) : ""));
- setDraftStatus(
- editingMember.status === "active" || editingMember.status === "pending" || editingMember.status === "suspended"
- ? editingMember.status
- : "suspended",
- );
- setDraftGrants(new Set(editingMember.grants.map((grant) => grant.permissionKey as PermissionKey)));
- setError(null);
- }, [editingMember]);
-
- if (query.loading && !query.data) {
- return ;
- }
-
- if (query.error) {
- return (
-
-
Could not load company members.
-
{query.error.message}
-
- );
- }
-
- const pendingHumans = members.filter((member) => member.principalType === "user" && member.status === "pending");
- const activeHumans = members.filter((member) => member.principalType === "user" && member.status !== "pending");
- const agentMembers = members.filter((member) => member.principalType === "agent");
- const closeEditor = () => {
- setEditingMemberId(null);
- setError(null);
- };
-
- async function save() {
- if (!editingMember) return;
- setBusy(true);
- setError(null);
- try {
- await saveMemberAccess({
- companyId,
- memberId: editingMember.id,
- membershipRole: draftRole || null,
- status: draftStatus,
- grants: [...draftGrants],
- });
- query.refresh();
- setEditingMemberId(null);
- } catch (err) {
- setError(getErrorMessage(err));
- } finally {
- setBusy(false);
- }
- }
-
- return (
-
-
-
-
-
-
-
Members
-
Company access
-
-
query.refresh()}>Refresh
-
-
- Roles, membership status, and explicit permission grants for humans and agents in this company.
-
- {members.length === 0 ? (
-
No company members yet.
- ) : null}
-
-
-
-
-
- {editingMember && editingProfile ? (
-
-
-
{editingProfile.label}
-
{editingProfile.secondary}
-
-
-
- Company role
- setDraftRole(event.target.value as HumanCompanyMembershipRole | "")}
- >
- Unset
- {Object.entries(HUMAN_ROLE_LABELS).map(([value, label]) => (
- {label}
- ))}
-
-
-
- Membership status
- setDraftStatus(event.target.value as MemberEditableStatus)}
- >
- Active
- Pending
- Suspended
-
-
-
-
-
-
- Implicit grants from role
-
-
-
- {draftRole
- ? `${HUMAN_ROLE_LABELS[draftRole]} already includes these permissions automatically.`
- : "No role selected, so this member has no implicit grants right now."}
-
- {implicitGrantKeys.length > 0 ? (
-
- {implicitGrantKeys.map((permissionKey) => (
-
- ))}
-
- ) : null}
-
-
-
-
Explicit grants
-
- Explicit grants persist when the role changes. Scoped assignment grants are managed in the policy editor below.
-
-
- {MEMBER_PERMISSION_KEYS.map((permissionKey) => {
- const isImplicit = implicitGrantKeys.includes(permissionKey);
- const isChecked = draftGrants.has(permissionKey);
- return (
-
-
- {
- setDraftGrants((current) => {
- const next = new Set(current);
- if (event.target.checked) next.add(permissionKey);
- else next.delete(permissionKey);
- return next;
- });
- }}
- />
-
- {PERMISSION_LABELS[permissionKey]}
-
- {permissionKey}
-
-
-
- {isImplicit && !isChecked ? (
-
Included implicitly by the {draftRole ? HUMAN_ROLE_LABELS[draftRole] : "selected"} role.
- ) : null}
- {isChecked ? (
-
Stored explicitly for this member.
- ) : null}
-
- );
- })}
-
-
-
- {error ?
Could not save: {error}
: null}
-
-
-
- Cancel
-
- void save()}>
- {busy ? : "Save access"}
-
-
-
- ) : null}
-
- );
-}
-
-function MembersTable({
- members,
- agents,
- label,
- emptyLabel,
- onEdit,
-}: {
- members: MemberRecord[];
- agents: AgentRecord[];
- label: string;
- emptyLabel: string;
- onEdit: (memberId: string) => void;
-}) {
- const columns: DataTableColumn[] = [
- { key: "principal", header: "Principal", render: (_value, row) => row.principal },
- { key: "role", header: "Role", render: (_value, row) => row.role, width: "140px" },
- { key: "status", header: "Status", render: (_value, row) => row.status, width: "120px" },
- { key: "grants", header: "Grants", render: (_value, row) => row.grants },
- { key: "action", header: "", render: (_value, row) => row.action, width: "80px" },
- ];
- const rows: MemberTableRow[] = members.map((member) => {
- const profile = profileForPrincipal(member, agents);
- return {
- id: member.id,
- member,
- principal: (
-
- {profile.label}
- {profile.secondary}
-
- ),
- role: member.membershipRole ? formatMode(member.membershipRole) : "Unset",
- status: ,
- grants: `${member.grants.length} explicit grant${member.grants.length === 1 ? "" : "s"}`,
- action: onEdit(member.id)}>Edit ,
- };
- });
- return (
-
-
- {label}
- 0 ? "info" : "pending"} />
-
-
>[]}
- rows={rows as unknown as Record[]}
- emptyMessage={emptyLabel}
- />
-
- );
-}
-
-function GrantRows({ grants }: { grants: AdvancedPolicyData["actorGrants"] }) {
- if (grants.length === 0) {
- return No assignment grants for this actor yet.
;
- }
- return (
-
- {grants.map((grant, index) => (
-
-
- {formatScope(grant.scope)}
-
- ))}
-
- );
-}
-
-function CurrentAgentPolicy({ policy }: { policy: AdvancedPolicyData["agentPolicy"] }) {
- if (!policy?.policy) {
- return No saved policy. Saving below will create one.
;
- }
- const visibilityMode = getPolicyString(policy.policy, "agentVisibility", "mode", "discoverable");
- const assignmentMode = getPolicyString(policy.policy, "assignmentPolicy", "mode", "company_default");
- const requiresApproval = getPolicyBoolean(policy.policy, "protectedAgent", "requiresApproval", false);
- const approvalReason = getPolicyString(policy.policy, "protectedAgent", "approvalReason", "");
- return (
-
- {formatMode(visibilityMode)}
-
- ),
- },
- {
- label: "Assignment",
- value: (
-
- {formatMode(assignmentMode)}
-
- ),
- },
- {
- label: "Protected agent",
- value: requiresApproval
- ? `Requires approval${approvalReason ? `: "${approvalReason}"` : ""}`
- : "No approval required",
- },
- ]}
- />
- );
-}
-
-function DecisionCard({ title, decision }: { title: string; decision: DecisionRecord | null }) {
- if (!decision) return null;
- return (
-
-
-
{decision.explanation}
-
-
-
- );
-}
-
-function AuthorizationAudit({
- entries,
- agents,
-}: {
- entries: AuditEntry[];
- agents: AgentRecord[];
-}) {
- type AuditRow = {
- id: string;
- time: string;
- actor: ReactNode;
- action: string;
- resource: string;
- decision: ReactNode;
- details: ReactNode;
- };
- const columns: DataTableColumn[] = [
- { key: "time", header: "Time", render: (_value, row) => row.time, width: "170px" },
- { key: "actor", header: "Actor", render: (_value, row) => row.actor },
- { key: "action", header: "Action", render: (_value, row) => row.action },
- { key: "resource", header: "Resource", render: (_value, row) => row.resource },
- { key: "decision", header: "Decision", render: (_value, row) => row.decision, width: "120px" },
- { key: "details", header: "Details", render: (_value, row) => row.details },
- ];
- const rows: AuditRow[] = entries.map((entry) => {
- const agent = entry.actorType === "agent" ? agents.find((candidate) => candidate.id === entry.actorId) : null;
- const decision = entry.details?.decision === "deny" ? false : entry.details?.decision === "allow" ? true : null;
- return {
- id: entry.id,
- time: formatDate(entry.createdAt),
- actor: (
-
- {agent?.name ?? formatMode(entry.actorType)}
- {entry.actorId}
-
- ),
- action: formatPermission(entry.action),
- resource: `${entry.entityType} / ${entry.entityId}`,
- decision: decision === null
- ?
- : ,
- details: ,
- };
- });
- return (
- >[]}
- rows={rows as unknown as Record[]}
- emptyMessage="No authorization decisions in this filter window yet. Adjust the filters above to broaden the audit search."
- />
- );
-}
-
-function AdvancedPolicyEditor({ companyId }: { companyId: string }) {
- const saveAgentPolicy = usePluginAction("saveAgentPolicy");
- const saveAssignmentGrant = usePluginAction("saveAssignmentGrant");
- const [actorAgentId, setActorAgentId] = useState("");
- const [targetAgentId, setTargetAgentId] = useState("");
- const [projectId, setProjectId] = useState("");
- const [issueId, setIssueId] = useState("");
- const [auditAction, setAuditAction] = useState("");
- const [auditActorType, setAuditActorType] = useState("");
- const [auditEntityType, setAuditEntityType] = useState("");
- const [auditEntityId, setAuditEntityId] = useState("");
- const [auditDecision, setAuditDecision] = useState("");
- const [visibilityMode, setVisibilityMode] = useState("discoverable");
- const [assignmentMode, setAssignmentMode] = useState("company_default");
- const [requiresApproval, setRequiresApproval] = useState(false);
- const [approvalReason, setApprovalReason] = useState("");
- const [grantMode, setGrantMode] = useState("scoped_agent");
- const [busyAction, setBusyAction] = useState(null);
- const [lastResult, setLastResult] = useState(null);
- const params = useMemo(() => ({
- companyId,
- actorAgentId,
- targetAgentId,
- projectId,
- issueId,
- auditAction,
- auditActorType,
- auditEntityType,
- auditEntityId,
- auditDecision,
- }), [companyId, actorAgentId, targetAgentId, projectId, issueId, auditAction, auditActorType, auditEntityType, auditEntityId, auditDecision]);
- const query = usePluginData("advancedPolicy", params);
- const data = query.data;
-
- useEffect(() => {
- if (!data) return;
- if (!actorAgentId && data.selected.actorAgentId) setActorAgentId(data.selected.actorAgentId);
- if (!targetAgentId && data.selected.targetAgentId) setTargetAgentId(data.selected.targetAgentId);
- if (!projectId && data.selected.projectId) setProjectId(data.selected.projectId);
- if (!issueId && data.selected.issueId) setIssueId(data.selected.issueId);
- }, [actorAgentId, data, issueId, projectId, targetAgentId]);
-
- useEffect(() => {
- const policy = data?.agentPolicy?.policy;
- setVisibilityMode(getPolicyString(policy, "agentVisibility", "mode", "discoverable"));
- setAssignmentMode(getPolicyString(policy, "assignmentPolicy", "mode", "company_default"));
- setRequiresApproval(getPolicyBoolean(policy, "protectedAgent", "requiresApproval", false));
- setApprovalReason(getPolicyString(policy, "protectedAgent", "approvalReason", ""));
- }, [data?.agentPolicy?.resourceId, data?.agentPolicy?.updatedAt]);
-
- const issueOptions = useMemo(
- () => (data?.issues ?? []).filter((issue) => !projectId || issue.projectId === projectId),
- [data?.issues, projectId],
- );
-
- async function run(label: string, action: () => Promise) {
- setBusyAction(label);
- try {
- const result = await action();
- setLastResult(result);
- query.refresh();
- } catch (error) {
- setLastResult({ error: getErrorMessage(error) });
- } finally {
- setBusyAction(null);
- }
- }
-
- if (query.loading && !data) {
- return ;
- }
-
- if (query.error) {
- return (
-
-
Advanced policy APIs unavailable.
-
{query.error.message}
-
- );
- }
-
- const hasPreviewSelection = Boolean(actorAgentId && targetAgentId);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
Policy preview
-
Check assignment decisions before saving policy changes
-
-
-
- Actor agent
- setActorAgentId(selection.assigneeAgentId ?? "")}
- />
-
-
- Target agent
- setTargetAgentId(selection.assigneeAgentId ?? "")}
- />
-
-
- Project scope
-
-
-
- Issue context
- setIssueId(event.target.value)}>
- No issue
- {issueOptions.map((issue) => {issue.title} )}
-
-
-
- {!hasPreviewSelection ? (
-
Select an actor and target agent to preview a policy decision.
- ) : (
-
-
-
-
- )}
-
-
-
-
-
Agent Visibility
-
- Directory mode
- setVisibilityMode(event.target.value)}>
- Discoverable
- Private
-
-
-
- Assignment mode
- setAssignmentMode(event.target.value)}>
- Company default
- Protected
-
-
-
- setRequiresApproval(event.target.checked)} />
- Require approval for protected assignment
-
-
- Approval reason
- setApprovalReason(event.target.value)} />
-
-
void run("policy", () => saveAgentPolicy({
- companyId,
- agentId: targetAgentId,
- visibilityMode,
- assignmentMode,
- requiresApproval,
- approvalReason,
- }))}
- >
- {busyAction === "policy" ? : "Save agent policy"}
-
-
-
-
-
Assignment Policy
-
- Grant mode
- setGrantMode(event.target.value)}>
- Scoped to selected target
- Broad assignment
- Clear assignment grants
-
-
-
void run("grants", () => saveAssignmentGrant({
- companyId,
- actorAgentId,
- targetAgentId,
- projectId,
- mode: grantMode,
- }))}
- >
- {busyAction === "grants" ? : "Save assignment grants"}
-
-
-
-
-
-
-
Current Agent Policy
-
-
-
-
-
-
-
Authorization Audit
-
Recent authorization decisions
-
-
query.refresh()}>Refresh
-
-
-
- Action
- setAuditAction(event.target.value)} />
-
-
- Actor type
- setAuditActorType(event.target.value)}>
- Any actor
- Agent
- User
- Plugin
- System
-
-
-
- Resource type
- setAuditEntityType(event.target.value)} />
-
-
- Resource id
- setAuditEntityId(event.target.value)} />
-
-
- Decision
- setAuditDecision(event.target.value)}>
- Any decision
- Allow
- Deny
-
-
-
-
-
-
- {lastResult ?
: null}
-
- );
-}
-
-function EePermissionsCompanySettingsPageContent(_props: PluginCompanySettingsPageProps) {
- const hostContext = useHostContext();
- const companyId = hostContext.companyId;
- const overview = usePluginData("overview", companyId ? { companyId } : {});
- const activate = usePluginAction("activateLicense");
- const deactivate = usePluginAction("deactivateLicense");
- const [activationBusy, setActivationBusy] = useState(false);
-
- if (!companyId) return ;
-
- if (overview.loading && !overview.data) {
- return (
-
- );
- }
-
- if (overview.error) {
- return (
-
-
-
Permissions
-
Could not load permissions
-
- {overview.error.code}: {overview.error.message}
-
-
-
- );
- }
-
- const data = overview.data;
- if (!data) {
- return (
-
-
-
No permissions data returned yet.
-
-
- );
- }
-
- if (data.license.status !== "active") {
- return (
- {
- setActivationBusy(true);
- void activate({ companyId })
- .then(() => overview.refresh())
- .finally(() => setActivationBusy(false));
- }}
- />
- );
- }
-
- return (
-
-
-
-
Advanced policy editing is active
-
-
- About enforcement
-
- Policy data stays in core. If this plugin is unavailable later, existing restrictions remain server-enforced.
-
-
-
-
- {
- setActivationBusy(true);
- void deactivate({ companyId })
- .then(() => overview.refresh())
- .finally(() => setActivationBusy(false));
- }}
- >
- {activationBusy ? : "Deactivate"}
-
-
-
-
-
-
-
- );
-}
-
-export function EePermissionsCompanySettingsPage(props: PluginCompanySettingsPageProps) {
- return (
- The Paperclip EE permissions UI could not render.}>
-
-
- );
-}
diff --git a/packages/plugins/paperclip-ee-permissions/src/ui/index.tsx b/packages/plugins/paperclip-ee-permissions/src/ui/index.tsx
deleted file mode 100644
index 3860a02129e..00000000000
--- a/packages/plugins/paperclip-ee-permissions/src/ui/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { EePermissionsCompanySettingsPage } from "./app.js";
diff --git a/packages/plugins/paperclip-ee-permissions/src/worker.ts b/packages/plugins/paperclip-ee-permissions/src/worker.ts
deleted file mode 100644
index 38c580cc094..00000000000
--- a/packages/plugins/paperclip-ee-permissions/src/worker.ts
+++ /dev/null
@@ -1,574 +0,0 @@
-import {
- definePlugin,
- runWorker,
- PLUGIN_RPC_ERROR_CODES,
- type HumanCompanyMembershipRole,
- type MembershipStatus,
- type PluginAccessMember,
- type PluginAuthorizationAuditEntry,
- type PluginAuthorizationPolicySummary,
- type PluginContext,
- type Issue,
- type PermissionKey,
- type PrincipalPermissionGrant,
-} from "@paperclipai/plugin-sdk";
-
-const LICENSE_STATE_SCOPE = "company";
-const LICENSE_STATE_KEY = "license";
-
-/**
- * Worker-visible license state. Phase 3A scaffolds a deterministic stub:
- * the plugin defaults to "unlicensed" unless an operator has explicitly
- * activated the EE permissions mode for the company. Later phases replace
- * this with a real license check tied to a core-owned advanced-mode flag.
- */
-export type EePermissionsLicense = {
- status: "active" | "inactive";
- activatedAt?: string;
- activatedByUserId?: string | null;
- note?: string | null;
-};
-
-const DEFAULT_LICENSE: EePermissionsLicense = { status: "inactive" };
-
-/**
- * Shape returned by `getData("overview", { companyId })`. The UI uses this
- * as the single source of truth for whether to render the advanced controls
- * or one of the deterministic missing-state fallbacks.
- */
-export type EePermissionsOverview = {
- companyId: string;
- license: EePermissionsLicense;
- policySummary: PluginAuthorizationPolicySummary | null;
- warnings: Array<{ code: string; message: string }>;
-};
-
-type AssignmentGrantMode = "broad" | "scoped_agent" | "clear";
-
-const HUMAN_ROLE_VALUES: readonly HumanCompanyMembershipRole[] = ["owner", "admin", "operator", "viewer"];
-const MEMBER_EDITABLE_STATUSES: ReadonlyArray> = [
- "pending",
- "active",
- "suspended",
-];
-const NON_MEMBER_PERMISSION_KEYS: ReadonlySet = new Set([
- "tasks:assign_scope",
-]);
-
-export type EePermissionsMemberAccessData = {
- companyId: string;
- warnings: Array<{ code: string; message: string }>;
- members: PluginAccessMember[];
- agents: Awaited>;
-};
-
-export type EePermissionsAdvancedPolicyData = {
- companyId: string;
- summary: PluginAuthorizationPolicySummary | null;
- warnings: Array<{ code: string; message: string }>;
- agents: Awaited>;
- issues: Issue[];
- selected: {
- actorAgentId: string | null;
- targetAgentId: string | null;
- projectId: string | null;
- issueId: string | null;
- };
- agentPolicy: Awaited>;
- actorGrants: PrincipalPermissionGrant[];
- preview: Awaited> | null;
- explanation: Awaited> | null;
- auditEntries: PluginAuthorizationAuditEntry[];
-};
-
-function readCompanyId(params: Record): string {
- const value = params.companyId;
- if (typeof value !== "string" || value.trim().length === 0) {
- throw new Error("companyId is required");
- }
- return value;
-}
-
-function readNumber(value: unknown, fallback: number): number {
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
-}
-
-function readStringOrUndefined(value: unknown): string | undefined {
- return typeof value === "string" && value.length > 0 ? value : undefined;
-}
-
-function readString(params: Record, key: string): string {
- const value = params[key];
- return typeof value === "string" ? value : "";
-}
-
-function readBoolean(params: Record, key: string, fallback = false): boolean {
- const value = params[key];
- return typeof value === "boolean" ? value : fallback;
-}
-
-function readGrantMode(value: unknown): AssignmentGrantMode {
- if (value === "broad" || value === "scoped_agent" || value === "clear") return value;
- return "scoped_agent";
-}
-
-function readHumanRole(value: unknown): HumanCompanyMembershipRole | null {
- if (value === null) return null;
- if (typeof value !== "string") return null;
- if (value === "") return null;
- return (HUMAN_ROLE_VALUES as readonly string[]).includes(value)
- ? (value as HumanCompanyMembershipRole)
- : null;
-}
-
-function readEditableStatus(value: unknown): Extract {
- if (typeof value === "string" && (MEMBER_EDITABLE_STATUSES as readonly string[]).includes(value)) {
- return value as Extract;
- }
- return "active";
-}
-
-function readPermissionKeyArray(value: unknown): PermissionKey[] {
- if (!Array.isArray(value)) return [];
- const out: PermissionKey[] = [];
- for (const entry of value) {
- if (typeof entry === "string" && !out.includes(entry as PermissionKey)) {
- out.push(entry as PermissionKey);
- }
- }
- return out;
-}
-
-function withoutAssignmentGrants(grants: PrincipalPermissionGrant[]) {
- return grants
- .filter((grant) => grant.permissionKey !== "tasks:assign" && grant.permissionKey !== "tasks:assign_scope")
- .map((grant) => ({
- permissionKey: grant.permissionKey as PermissionKey,
- scope: grant.scope && typeof grant.scope === "object" ? grant.scope as Record : null,
- }));
-}
-
-function isCapabilityDenied(error: unknown): boolean {
- if (!error) return false;
- const code = (error as { code?: unknown }).code;
- if (code === PLUGIN_RPC_ERROR_CODES.CAPABILITY_DENIED) return true;
- if (code === "CAPABILITY_DENIED") return true;
- const message = error instanceof Error ? error.message : String(error);
- return /missing required capability/i.test(message);
-}
-
-function formatError(error: unknown): { code: string; message: string } {
- if (isCapabilityDenied(error)) {
- return {
- code: "CAPABILITY_DENIED",
- message: error instanceof Error ? error.message : String(error),
- };
- }
- return {
- code: "WORKER_ERROR",
- message: error instanceof Error ? error.message : String(error),
- };
-}
-
-async function readLicense(
- ctx: PluginContext,
- companyId: string,
-): Promise {
- const value = (await ctx.state.get({
- scopeKind: LICENSE_STATE_SCOPE,
- scopeId: companyId,
- stateKey: LICENSE_STATE_KEY,
- })) as EePermissionsLicense | null;
- if (!value || typeof value !== "object") return DEFAULT_LICENSE;
- return value.status === "active" ? value : DEFAULT_LICENSE;
-}
-
-const plugin = definePlugin({
- async setup(ctx) {
- ctx.data.register("overview", async (params) => {
- const companyId = readCompanyId(params ?? {});
- const license = await readLicense(ctx, companyId);
- const warnings: EePermissionsOverview["warnings"] = [];
- if (license.status !== "active") {
- return {
- companyId,
- license,
- policySummary: null,
- warnings,
- } satisfies EePermissionsOverview;
- }
- let policySummary: PluginAuthorizationPolicySummary | null = null;
- try {
- policySummary = await ctx.authorization.policies.summary(companyId);
- } catch (error) {
- warnings.push(formatError(error));
- }
- return {
- companyId,
- license,
- policySummary,
- warnings,
- } satisfies EePermissionsOverview;
- });
-
- ctx.data.register("members", async (params) => {
- const companyId = readCompanyId(params ?? {});
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") return [] as PluginAccessMember[];
- try {
- return await ctx.access.members.list({ companyId });
- } catch (error) {
- if (isCapabilityDenied(error)) return [] as PluginAccessMember[];
- throw error;
- }
- });
-
- ctx.data.register("memberAccess", async (params) => {
- const companyId = readCompanyId(params ?? {});
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") {
- return {
- companyId,
- warnings: [],
- members: [],
- agents: [],
- } satisfies EePermissionsMemberAccessData;
- }
- const warnings: EePermissionsMemberAccessData["warnings"] = [];
- let members: PluginAccessMember[] = [];
- let agents: EePermissionsMemberAccessData["agents"] = [];
- try {
- members = await ctx.access.members.list({ companyId });
- } catch (error) {
- warnings.push(formatError(error));
- }
- try {
- agents = await ctx.agents.list({ companyId, limit: 200, offset: 0 });
- } catch (error) {
- warnings.push(formatError(error));
- }
- return { companyId, warnings, members, agents } satisfies EePermissionsMemberAccessData;
- });
-
- ctx.data.register("grants", async (params) => {
- const input = params ?? {};
- const companyId = readCompanyId(input);
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") return [] as PrincipalPermissionGrant[];
- try {
- return await ctx.authorization.grants.list({
- companyId,
- principalType: readStringOrUndefined(input.principalType) as
- | PrincipalPermissionGrant["principalType"]
- | undefined,
- principalId: readStringOrUndefined(input.principalId),
- });
- } catch (error) {
- if (isCapabilityDenied(error)) return [] as PrincipalPermissionGrant[];
- throw error;
- }
- });
-
- ctx.data.register("audit", async (params) => {
- const input = params ?? {};
- const companyId = readCompanyId(input);
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") return [] as PluginAuthorizationAuditEntry[];
- try {
- return await ctx.authorization.audit.search({
- companyId,
- action: readStringOrUndefined(input.action),
- actorType: readStringOrUndefined(input.actorType),
- actorId: readStringOrUndefined(input.actorId),
- entityType: readStringOrUndefined(input.entityType),
- entityId: readStringOrUndefined(input.entityId),
- decision: readStringOrUndefined(input.decision),
- limit: readNumber(input.limit, 25),
- offset: readNumber(input.offset, 0),
- });
- } catch (error) {
- if (isCapabilityDenied(error)) return [] as PluginAuthorizationAuditEntry[];
- throw error;
- }
- });
-
- ctx.data.register("advancedPolicy", async (params) => {
- const input = params ?? {};
- const companyId = readCompanyId(input);
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") {
- return {
- companyId,
- summary: null,
- warnings: [],
- agents: [],
- issues: [],
- selected: {
- actorAgentId: null,
- targetAgentId: null,
- projectId: null,
- issueId: null,
- },
- agentPolicy: null,
- actorGrants: [],
- preview: null,
- explanation: null,
- auditEntries: [],
- } satisfies EePermissionsAdvancedPolicyData;
- }
-
- const warnings: EePermissionsAdvancedPolicyData["warnings"] = [];
- const [summary, agents, issues] = await Promise.all([
- ctx.authorization.policies.summary(companyId).catch((error) => {
- warnings.push(formatError(error));
- return null;
- }),
- ctx.agents.list({ companyId, limit: 200, offset: 0 }).catch((error) => {
- warnings.push(formatError(error));
- return [];
- }),
- ctx.issues.list({ companyId, limit: 100, offset: 0 }).catch((error) => {
- warnings.push(formatError(error));
- return [] as Issue[];
- }),
- ]);
- const actorAgent = agents.find((agent) => agent.id === readString(input, "actorAgentId")) ?? agents[0] ?? null;
- const targetAgent = agents.find((agent) => agent.id === readString(input, "targetAgentId"))
- ?? agents.find((agent) => agent.id !== actorAgent?.id)
- ?? agents[0]
- ?? null;
- const selectedProjectId = readString(input, "projectId") || null;
- const selectedIssueId = readString(input, "issueId") || null;
- const [agentPolicy, actorGrants, preview, explanation, auditEntries] = await Promise.all([
- targetAgent
- ? ctx.authorization.policies.get({ companyId, resourceType: "agent", resourceId: targetAgent.id }).catch((error) => {
- warnings.push(formatError(error));
- return null;
- })
- : Promise.resolve(null),
- actorAgent
- ? ctx.authorization.grants.list({ companyId, principalType: "agent", principalId: actorAgent.id }).catch((error) => {
- warnings.push(formatError(error));
- return [] as PrincipalPermissionGrant[];
- })
- : Promise.resolve([] as PrincipalPermissionGrant[]),
- actorAgent && targetAgent
- ? ctx.authorization.policies.previewAssignment({
- companyId,
- actor: { type: "agent", agentId: actorAgent.id, companyId },
- target: {
- issueId: selectedIssueId,
- projectId: selectedProjectId,
- assigneeAgentId: targetAgent.id,
- },
- }).catch((error) => {
- warnings.push(formatError(error));
- return null;
- })
- : Promise.resolve(null),
- actorAgent && targetAgent
- ? ctx.authorization.policies.explainAssignment({
- companyId,
- actor: { type: "agent", agentId: actorAgent.id, companyId },
- target: {
- issueId: selectedIssueId,
- projectId: selectedProjectId,
- assigneeAgentId: targetAgent.id,
- },
- }).catch((error) => {
- warnings.push(formatError(error));
- return null;
- })
- : Promise.resolve(null),
- ctx.authorization.audit.search({
- companyId,
- action: readStringOrUndefined(input.action),
- actorType: readStringOrUndefined(input.actorType),
- actorId: readStringOrUndefined(input.actorId),
- entityType: readStringOrUndefined(input.entityType),
- entityId: readStringOrUndefined(input.entityId),
- decision: readStringOrUndefined(input.decision),
- limit: 25,
- offset: 0,
- }).catch((error) => {
- warnings.push(formatError(error));
- return [] as PluginAuthorizationAuditEntry[];
- }),
- ]);
-
- return {
- companyId,
- summary,
- warnings,
- agents,
- issues,
- selected: {
- actorAgentId: actorAgent?.id ?? null,
- targetAgentId: targetAgent?.id ?? null,
- projectId: selectedProjectId,
- issueId: selectedIssueId,
- },
- agentPolicy,
- actorGrants,
- preview,
- explanation,
- auditEntries,
- } satisfies EePermissionsAdvancedPolicyData;
- });
-
- ctx.actions.register("activateLicense", async (params) => {
- const input = params ?? {};
- const companyId = readCompanyId(input);
- const license: EePermissionsLicense = {
- status: "active",
- activatedAt: new Date().toISOString(),
- activatedByUserId: readStringOrUndefined(input.activatedByUserId) ?? null,
- note: readStringOrUndefined(input.note) ?? null,
- };
- await ctx.state.set(
- {
- scopeKind: LICENSE_STATE_SCOPE,
- scopeId: companyId,
- stateKey: LICENSE_STATE_KEY,
- },
- license,
- );
- return license;
- });
-
- ctx.actions.register("deactivateLicense", async (params) => {
- const companyId = readCompanyId(params ?? {});
- await ctx.state.delete({
- scopeKind: LICENSE_STATE_SCOPE,
- scopeId: companyId,
- stateKey: LICENSE_STATE_KEY,
- });
- return DEFAULT_LICENSE;
- });
-
- ctx.actions.register("saveMemberAccess", async (params) => {
- const input = params ?? {};
- const companyId = readCompanyId(input);
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") {
- throw new Error("Paperclip EE permissions mode is not active for this company");
- }
- const memberId = readString(input, "memberId");
- if (!memberId) throw new Error("memberId is required");
- const member = await ctx.access.members.get(memberId, companyId);
- if (!member) throw new Error(`Membership not found: ${memberId}`);
- const membershipRole = readHumanRole(input.membershipRole);
- const status = readEditableStatus(input.status);
- const explicitGrants = readPermissionKeyArray(input.grants)
- .filter((key) => !NON_MEMBER_PERMISSION_KEYS.has(key));
-
- const updatedMember = await ctx.access.members.update(
- memberId,
- { membershipRole, status },
- companyId,
- );
-
- const existing = await ctx.authorization.grants.list({
- companyId,
- principalType: member.principalType,
- principalId: member.principalId,
- });
- const carriedOverNonMemberGrants = existing
- .filter((grant) => NON_MEMBER_PERMISSION_KEYS.has(grant.permissionKey as PermissionKey))
- .map((grant) => ({
- permissionKey: grant.permissionKey as PermissionKey,
- scope: grant.scope && typeof grant.scope === "object" ? grant.scope as Record : null,
- }));
- const nextGrants = [
- ...explicitGrants.map((permissionKey) => ({ permissionKey, scope: null as Record | null })),
- ...carriedOverNonMemberGrants,
- ];
-
- const grants = await ctx.authorization.grants.set({
- companyId,
- principalType: member.principalType,
- principalId: member.principalId,
- grants: nextGrants,
- });
-
- return {
- member: { ...updatedMember, grants },
- };
- });
-
- ctx.actions.register("saveAgentPolicy", async (params) => {
- const input = params ?? {};
- const companyId = readCompanyId(input);
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") throw new Error("Paperclip EE permissions mode is not active for this company");
- const agentId = readString(input, "agentId");
- if (!agentId) throw new Error("agentId is required");
- const visibilityMode = readString(input, "visibilityMode") === "private" ? "private" : "discoverable";
- const assignmentMode = readString(input, "assignmentMode") === "protected" ? "protected" : "company_default";
- const approvalReason = readString(input, "approvalReason").trim();
- return await ctx.authorization.policies.update({
- companyId,
- resourceType: "agent",
- resourceId: agentId,
- policy: {
- agentVisibility: {
- mode: visibilityMode,
- hiddenFromDefaultDirectory: visibilityMode === "private",
- },
- assignmentPolicy: {
- mode: assignmentMode,
- protectedAgentRequiresApproval: assignmentMode === "protected",
- },
- protectedAgent: {
- requiresApproval: readBoolean(input, "requiresApproval", assignmentMode === "protected"),
- approvalReason: approvalReason || null,
- },
- managedBy: "paperclip-ee-permissions",
- },
- });
- });
-
- ctx.actions.register("saveAssignmentGrant", async (params) => {
- const input = params ?? {};
- const companyId = readCompanyId(input);
- const license = await readLicense(ctx, companyId);
- if (license.status !== "active") throw new Error("Paperclip EE permissions mode is not active for this company");
- const actorAgentId = readString(input, "actorAgentId");
- const targetAgentId = readString(input, "targetAgentId");
- const projectId = readString(input, "projectId");
- const mode = readGrantMode(input.mode);
- if (!actorAgentId) throw new Error("actorAgentId is required");
- const existing = await ctx.authorization.grants.list({
- companyId,
- principalType: "agent",
- principalId: actorAgentId,
- });
- const grants = withoutAssignmentGrants(existing);
- if (mode === "broad") {
- grants.push({ permissionKey: "tasks:assign", scope: null });
- } else if (mode === "scoped_agent") {
- if (!targetAgentId) throw new Error("targetAgentId is required for scoped assignment grants");
- grants.push({
- permissionKey: "tasks:assign_scope",
- scope: {
- assigneeAgentId: targetAgentId,
- ...(projectId ? { projectId } : {}),
- },
- });
- }
- return await ctx.authorization.grants.set({
- companyId,
- principalType: "agent",
- principalId: actorAgentId,
- grants,
- });
- });
- },
-
- async onHealth() {
- return { status: "ok", message: "Paperclip EE Permissions worker is running" };
- },
-});
-
-export default plugin;
-runWorker(plugin, import.meta.url);
diff --git a/packages/plugins/paperclip-ee-permissions/tests/plugin.spec.ts b/packages/plugins/paperclip-ee-permissions/tests/plugin.spec.ts
deleted file mode 100644
index d912b3468a9..00000000000
--- a/packages/plugins/paperclip-ee-permissions/tests/plugin.spec.ts
+++ /dev/null
@@ -1,394 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { createTestHarness } from "@paperclipai/plugin-sdk/testing";
-import manifest, { EXPORT_NAMES, SLOT_IDS } from "../src/manifest.js";
-import plugin, {
- type EePermissionsAdvancedPolicyData,
- type EePermissionsLicense,
- type EePermissionsMemberAccessData,
- type EePermissionsOverview,
-} from "../src/worker.js";
-
-const COMPANY_ID = "00000000-0000-4000-8000-000000000001";
-const ACTOR_AGENT_ID = "00000000-0000-4000-8000-0000000000a1";
-const TARGET_AGENT_ID = "00000000-0000-4000-8000-0000000000a2";
-const ISSUE_ID = "00000000-0000-4000-8000-0000000000b1";
-const MEMBER_USER_ID = "00000000-0000-4000-8000-0000000000c1";
-const MEMBER_RECORD_ID = "00000000-0000-4000-8000-0000000000d1";
-
-describe("paperclip-ee-permissions manifest", () => {
- it("declares the company settings page slot", () => {
- const slot = manifest.ui?.slots?.find(
- (entry) => entry.id === SLOT_IDS.companySettingsPage,
- );
- expect(slot).toBeDefined();
- expect(slot?.type).toBe("companySettingsPage");
- expect(slot?.exportName).toBe(EXPORT_NAMES.companySettingsPage);
- expect(slot?.routePath).toBe("permissions");
- });
-
- it("requires the access and authorization read capabilities", () => {
- for (const capability of [
- "access.members.read",
- "access.members.write",
- "issues.read",
- "authorization.grants.read",
- "authorization.grants.write",
- "authorization.policies.read",
- "authorization.policies.write",
- "authorization.audit.read",
- "ui.page.register",
- "instance.settings.register",
- "plugin.state.read",
- "plugin.state.write",
- ] as const) {
- expect(manifest.capabilities).toContain(capability);
- }
- });
-});
-
-describe("paperclip-ee-permissions worker", () => {
- it("returns an unlicensed overview by default", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- await plugin.definition.setup(harness.ctx);
- const overview = await harness.getData("overview", {
- companyId: COMPANY_ID,
- });
- expect(overview.companyId).toBe(COMPANY_ID);
- expect(overview.license.status).toBe("inactive");
- expect(overview.policySummary).toBeNull();
- expect(overview.warnings).toEqual([]);
- });
-
- it("activates and deactivates the per-company license", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- await plugin.definition.setup(harness.ctx);
-
- const activated = await harness.performAction(
- "activateLicense",
- { companyId: COMPANY_ID, activatedByUserId: "user-1" },
- );
- expect(activated.status).toBe("active");
- expect(activated.activatedByUserId).toBe("user-1");
-
- const overview = await harness.getData("overview", {
- companyId: COMPANY_ID,
- });
- expect(overview.license.status).toBe("active");
- expect(overview.policySummary).toMatchObject({
- companyId: COMPANY_ID,
- permissionsMode: "simple",
- advancedPolicyAvailable: false,
- });
-
- const deactivated = await harness.performAction(
- "deactivateLicense",
- { companyId: COMPANY_ID },
- );
- expect(deactivated.status).toBe("inactive");
-
- const reverted = await harness.getData("overview", {
- companyId: COMPANY_ID,
- });
- expect(reverted.license.status).toBe("inactive");
- expect(reverted.policySummary).toBeNull();
- });
-
- it("returns empty lists for grants/members when unlicensed", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- await plugin.definition.setup(harness.ctx);
- const members = await harness.getData("members", { companyId: COMPANY_ID });
- const grants = await harness.getData("grants", { companyId: COMPANY_ID });
- const audit = await harness.getData("audit", { companyId: COMPANY_ID });
- expect(members).toEqual([]);
- expect(grants).toEqual([]);
- expect(audit).toEqual([]);
- });
-
- it("falls back to empty data when a capability is missing", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities.filter(
- (capability) => capability !== "authorization.grants.read",
- ),
- });
- await plugin.definition.setup(harness.ctx);
- await harness.performAction("activateLicense", { companyId: COMPANY_ID });
- const grants = await harness.getData("grants", { companyId: COMPANY_ID });
- expect(grants).toEqual([]);
- });
-
- it("loads advanced policy data and previews through the authorization SDK", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- harness.seed({
- agents: [
- {
- id: ACTOR_AGENT_ID,
- companyId: COMPANY_ID,
- name: "Actor",
- role: "engineer",
- status: "active",
- adapterType: "process",
- adapterConfig: {},
- permissions: {},
- createdAt: new Date(),
- updatedAt: new Date(),
- } as any,
- {
- id: TARGET_AGENT_ID,
- companyId: COMPANY_ID,
- name: "Target",
- role: "engineer",
- status: "active",
- adapterType: "process",
- adapterConfig: {},
- permissions: {},
- createdAt: new Date(),
- updatedAt: new Date(),
- } as any,
- ],
- issues: [
- {
- id: ISSUE_ID,
- companyId: COMPANY_ID,
- title: "Preview issue",
- status: "todo",
- createdAt: new Date(),
- updatedAt: new Date(),
- } as any,
- ],
- });
- await plugin.definition.setup(harness.ctx);
- await harness.performAction("activateLicense", { companyId: COMPANY_ID });
-
- const data = await harness.getData("advancedPolicy", {
- companyId: COMPANY_ID,
- actorAgentId: ACTOR_AGENT_ID,
- targetAgentId: TARGET_AGENT_ID,
- issueId: ISSUE_ID,
- decision: "allow",
- });
-
- expect(data.agents).toHaveLength(2);
- expect(data.issues).toHaveLength(1);
- expect(data.selected).toMatchObject({
- actorAgentId: ACTOR_AGENT_ID,
- targetAgentId: TARGET_AGENT_ID,
- issueId: ISSUE_ID,
- });
- expect(data.preview).toMatchObject({ allowed: true });
- expect(data.explanation).toMatchObject({ allowed: true });
- });
-
- it("updates agent policies and assignment grants through SDK actions", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- await plugin.definition.setup(harness.ctx);
- await harness.performAction("activateLicense", { companyId: COMPANY_ID });
-
- const policy = await harness.performAction>("saveAgentPolicy", {
- companyId: COMPANY_ID,
- agentId: TARGET_AGENT_ID,
- visibilityMode: "private",
- assignmentMode: "protected",
- requiresApproval: true,
- approvalReason: "Sensitive production agent",
- });
- const grants = await harness.performAction("saveAssignmentGrant", {
- companyId: COMPANY_ID,
- actorAgentId: ACTOR_AGENT_ID,
- targetAgentId: TARGET_AGENT_ID,
- mode: "scoped_agent",
- });
-
- expect(policy.policy).toMatchObject({
- agentVisibility: { mode: "private" },
- protectedAgent: { requiresApproval: true },
- });
- expect(grants).toEqual([
- {
- principalType: "agent",
- principalId: ACTOR_AGENT_ID,
- permissionKey: "tasks:assign_scope",
- scope: { assigneeAgentId: TARGET_AGENT_ID },
- },
- ]);
- });
-
- it("returns empty member access while unlicensed", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- await plugin.definition.setup(harness.ctx);
- const data = await harness.getData("memberAccess", {
- companyId: COMPANY_ID,
- });
- expect(data.companyId).toBe(COMPANY_ID);
- expect(data.members).toEqual([]);
- expect(data.warnings).toEqual([]);
- });
-
- it("loads seeded company members once licensed", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- harness.seed({
- accessMembers: [
- {
- id: MEMBER_RECORD_ID,
- companyId: COMPANY_ID,
- principalType: "user",
- principalId: MEMBER_USER_ID,
- status: "active",
- membershipRole: "operator",
- grants: [],
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- },
- ],
- principalGrants: [
- {
- id: "grant-1",
- companyId: COMPANY_ID,
- principalType: "user",
- principalId: MEMBER_USER_ID,
- permissionKey: "users:invite",
- scope: null,
- grantedByUserId: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- ],
- });
- await plugin.definition.setup(harness.ctx);
- await harness.performAction("activateLicense", { companyId: COMPANY_ID });
-
- const data = await harness.getData("memberAccess", {
- companyId: COMPANY_ID,
- });
- expect(data.members).toHaveLength(1);
- const [member] = data.members;
- expect(member.principalId).toBe(MEMBER_USER_ID);
- expect(member.membershipRole).toBe("operator");
- expect(member.grants.map((grant) => grant.permissionKey)).toEqual(["users:invite"]);
- });
-
- it("saves member role, status, and grants atomically", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- harness.seed({
- accessMembers: [
- {
- id: MEMBER_RECORD_ID,
- companyId: COMPANY_ID,
- principalType: "user",
- principalId: MEMBER_USER_ID,
- status: "active",
- membershipRole: "viewer",
- grants: [],
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- },
- ],
- principalGrants: [
- // pre-existing scoped grant should survive a member-level save
- {
- id: "grant-scoped",
- companyId: COMPANY_ID,
- principalType: "user",
- principalId: MEMBER_USER_ID,
- permissionKey: "tasks:assign_scope",
- scope: { assigneeAgentId: TARGET_AGENT_ID },
- grantedByUserId: null,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- ],
- });
- await plugin.definition.setup(harness.ctx);
- await harness.performAction("activateLicense", { companyId: COMPANY_ID });
-
- const result = await harness.performAction<{
- member: EePermissionsMemberAccessData["members"][number];
- }>("saveMemberAccess", {
- companyId: COMPANY_ID,
- memberId: MEMBER_RECORD_ID,
- membershipRole: "operator",
- status: "active",
- grants: ["users:invite", "tasks:assign"],
- });
- expect(result.member.membershipRole).toBe("operator");
- expect(result.member.status).toBe("active");
- const grantKeys = result.member.grants.map((grant) => grant.permissionKey).sort();
- expect(grantKeys).toEqual(["tasks:assign", "tasks:assign_scope", "users:invite"]);
-
- const reread = await harness.getData("memberAccess", {
- companyId: COMPANY_ID,
- });
- const [updated] = reread.members;
- expect(updated.membershipRole).toBe("operator");
- expect(updated.grants.map((grant) => grant.permissionKey).sort()).toEqual([
- "tasks:assign",
- "tasks:assign_scope",
- "users:invite",
- ]);
- });
-
- it("refuses to save member access while unlicensed", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- harness.seed({
- accessMembers: [
- {
- id: MEMBER_RECORD_ID,
- companyId: COMPANY_ID,
- principalType: "user",
- principalId: MEMBER_USER_ID,
- status: "active",
- membershipRole: "viewer",
- grants: [],
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- },
- ],
- });
- await plugin.definition.setup(harness.ctx);
- await expect(
- harness.performAction("saveMemberAccess", {
- companyId: COMPANY_ID,
- memberId: MEMBER_RECORD_ID,
- membershipRole: "operator",
- status: "active",
- grants: [],
- }),
- ).rejects.toThrow(/permissions mode is not active/);
- });
-
- it("rejects requests that omit companyId", async () => {
- const harness = createTestHarness({
- manifest,
- capabilities: manifest.capabilities,
- });
- await plugin.definition.setup(harness.ctx);
- await expect(harness.getData("overview", {})).rejects.toThrow(/companyId/);
- });
-});
diff --git a/packages/plugins/paperclip-ee-permissions/tsconfig.json b/packages/plugins/paperclip-ee-permissions/tsconfig.json
deleted file mode 100644
index a697519ed9d..00000000000
--- a/packages/plugins/paperclip-ee-permissions/tsconfig.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
- "lib": [
- "ES2022",
- "DOM"
- ],
- "jsx": "react-jsx",
- "strict": true,
- "skipLibCheck": true,
- "declaration": true,
- "declarationMap": true,
- "sourceMap": true,
- "outDir": "dist",
- "rootDir": "."
- },
- "include": [
- "src",
- "tests"
- ],
- "exclude": [
- "dist",
- "node_modules"
- ]
-}
diff --git a/packages/plugins/paperclip-ee-permissions/vitest.config.ts b/packages/plugins/paperclip-ee-permissions/vitest.config.ts
deleted file mode 100644
index 649a293ef95..00000000000
--- a/packages/plugins/paperclip-ee-permissions/vitest.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { defineConfig } from "vitest/config";
-
-export default defineConfig({
- test: {
- include: ["tests/**/*.spec.ts"],
- environment: "node",
- },
-});
diff --git a/server/src/__tests__/authorization-service.test.ts b/server/src/__tests__/authorization-service.test.ts
index 0b3ee35c5a1..ab6a868275d 100644
--- a/server/src/__tests__/authorization-service.test.ts
+++ b/server/src/__tests__/authorization-service.test.ts
@@ -233,7 +233,7 @@ describeEmbeddedPostgres("authorization service", () => {
requiresApproval: true,
approvalReason: "Production deployment authority",
},
- managedBy: "paperclip-ee-permissions",
+ managedBy: "permissions-extension",
},
},
});
@@ -270,7 +270,7 @@ describeEmbeddedPostgres("authorization service", () => {
protectedAgent: {
requiresApproval: false,
},
- managedBy: "paperclip-ee-permissions",
+ managedBy: "permissions-extension",
},
},
});
diff --git a/server/src/__tests__/plugin-access-authorization-host-services.test.ts b/server/src/__tests__/plugin-access-authorization-host-services.test.ts
index ab20f4f8b8a..0e1648965d1 100644
--- a/server/src/__tests__/plugin-access-authorization-host-services.test.ts
+++ b/server/src/__tests__/plugin-access-authorization-host-services.test.ts
@@ -79,7 +79,7 @@ describeEmbeddedPostgres("plugin access and authorization host services", () =>
})
.returning()
.then((rows) => rows[0]!);
- const services = buildHostServices(db, pluginId, "paperclip-ee", createEventBusStub());
+ const services = buildHostServices(db, pluginId, "permissions-extension", createEventBusStub());
await expect(
services.authorization.setGrants({
@@ -97,7 +97,7 @@ describeEmbeddedPostgres("plugin access and authorization host services", () =>
it("redacts invite token hashes and sensitive defaults from plugin invite reads", async () => {
const company = await createCompany(db, "PAZ");
- const services = buildHostServices(db, pluginId, "paperclip-ee", createEventBusStub());
+ const services = buildHostServices(db, pluginId, "permissions-extension", createEventBusStub());
const created = await services.access.createInvite({
companyId: company.id,
@@ -124,7 +124,7 @@ describeEmbeddedPostgres("plugin access and authorization host services", () =>
it("filters authorization audit entries by allow or deny decision details", async () => {
const company = await createCompany(db, "PAU");
- const services = buildHostServices(db, pluginId, "paperclip-ee", createEventBusStub());
+ const services = buildHostServices(db, pluginId, "permissions-extension", createEventBusStub());
await db.insert(activityLog).values([
{
companyId: company.id,
@@ -198,7 +198,7 @@ describeEmbeddedPostgres("plugin access and authorization host services", () =>
membershipRole: "member",
});
- const services = buildHostServices(db, pluginId, "paperclip-ee", createEventBusStub());
+ const services = buildHostServices(db, pluginId, "permissions-extension", createEventBusStub());
const updatedPolicy = await services.authorization.updatePolicy({
companyId: company.id,
resourceType: "agent",
@@ -212,7 +212,7 @@ describeEmbeddedPostgres("plugin access and authorization host services", () =>
requiresApproval: true,
approvalReason: "Needs board approval",
},
- managedBy: "paperclip-ee-permissions",
+ managedBy: "permissions-extension",
},
});
const input = {
diff --git a/ui/src/components/CompanySettingsSidebar.test.tsx b/ui/src/components/CompanySettingsSidebar.test.tsx
index d10c4e401ea..6514cd4a99b 100644
--- a/ui/src/components/CompanySettingsSidebar.test.tsx
+++ b/ui/src/components/CompanySettingsSidebar.test.tsx
@@ -175,8 +175,8 @@ describe("CompanySettingsSidebar", () => {
exportName: "PermissionsPage",
routePath: "permissions",
pluginId: "plugin-1",
- pluginKey: "paperclip-ee",
- pluginDisplayName: "Paperclip EE",
+ pluginKey: "permissions-extension",
+ pluginDisplayName: "Permissions Extension",
pluginVersion: "0.1.0",
},
],
diff --git a/ui/src/pages/CompanyAccess.test.tsx b/ui/src/pages/CompanyAccess.test.tsx
index aaf4979e272..73f9e3f4fea 100644
--- a/ui/src/pages/CompanyAccess.test.tsx
+++ b/ui/src/pages/CompanyAccess.test.tsx
@@ -209,7 +209,7 @@ describe("CompanyAccess", () => {
expect(container.textContent).toContain("Manage the people who can work in Paperclip");
expect(container.textContent).toContain("Members can collaborate across the company by default");
- expect(container.textContent).toContain("Advanced permission controls are available in Paperclip EE");
+ expect(container.textContent).toContain("Core keeps this page focused on membership");
expect(container.textContent).toContain("Humans");
expect(container.textContent).toContain("Pending human joins");
expect(container.textContent).toContain("User account");
@@ -404,7 +404,7 @@ describe("CompanyAccess", () => {
});
});
- it("redirects legacy access deep links to the EE permissions route when installed", async () => {
+ it("redirects legacy access deep links to the permissions extension route when installed", async () => {
mockUsePluginSlots.mockReturnValue({
slots: [
{
@@ -412,7 +412,7 @@ describe("CompanyAccess", () => {
id: "permissions",
displayName: "Permissions",
routePath: "permissions",
- pluginKey: "paperclip-ee",
+ pluginKey: "permissions-extension",
},
],
isLoading: false,
@@ -440,7 +440,7 @@ describe("CompanyAccess", () => {
});
});
- it("shows a read-only EE unavailable fallback for legacy access deep links", async () => {
+ it("shows a read-only unavailable fallback for legacy access deep links", async () => {
const root = createRoot(container);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
@@ -456,7 +456,7 @@ describe("CompanyAccess", () => {
await flushReact();
expect(container.textContent).toContain("Advanced Permissions");
- expect(container.textContent).toContain("Paperclip EE unavailable");
+ expect(container.textContent).toContain("Advanced permissions unavailable");
expect(container.textContent).toContain("Open Members");
expect(container.textContent).toContain("Open Invites");
diff --git a/ui/src/pages/CompanyAccess.tsx b/ui/src/pages/CompanyAccess.tsx
index 0834e8c0266..9c43d5ae70a 100644
--- a/ui/src/pages/CompanyAccess.tsx
+++ b/ui/src/pages/CompanyAccess.tsx
@@ -244,7 +244,7 @@ export function CompanyAccess() {
Manage the people who can work in {selectedCompany?.name}. Members can collaborate across the company by default.
- Advanced permission controls are available in Paperclip EE. Core keeps this page focused on membership, invite approvals, and safe member removal.
+ Core keeps this page focused on membership, invite approvals, and safe member removal.
@@ -545,7 +545,7 @@ export function CompanyAccessLegacyRoute() {
}
if (isLoading) {
- return Checking for Paperclip EE permissions...
;
+ return Checking for advanced permission extensions...
;
}
return (
@@ -556,15 +556,15 @@ export function CompanyAccessLegacyRoute() {
Advanced Permissions
- Advanced access, scoped assignment, and explicit grant controls are now owned by the Paperclip EE Permissions plugin.
+ Advanced access, scoped assignment, and explicit grant controls are provided by installed company settings extensions.
-
Paperclip EE unavailable
+
Advanced permissions unavailable
- Core Paperclip keeps enforcing company boundaries and any existing restrictive policy data, but editing advanced permissions requires Paperclip EE.
+ Core Paperclip keeps enforcing company boundaries and any existing restrictive policy data, but editing advanced permissions requires an installed extension.
{errorMessage ? (
Plugin extensions unavailable: {errorMessage}
diff --git a/ui/src/pages/CompanySettingsPluginPage.test.tsx b/ui/src/pages/CompanySettingsPluginPage.test.tsx
index 5c22a35057e..12c5a8bb17a 100644
--- a/ui/src/pages/CompanySettingsPluginPage.test.tsx
+++ b/ui/src/pages/CompanySettingsPluginPage.test.tsx
@@ -91,8 +91,8 @@ describe("CompanySettingsPluginPage", () => {
exportName: "PermissionsPage",
routePath: "permissions",
pluginId: "plugin-1",
- pluginKey: "paperclip-ee",
- pluginDisplayName: "Paperclip EE",
+ pluginKey: "permissions-extension",
+ pluginDisplayName: "Permissions Extension",
pluginVersion: "0.1.0",
},
],