Status: proposed complete spec for the post-V1 plugin system
This document is the complete specification for Paperclip's plugin and extension architecture.
It expands the brief plugin notes in [doc/SPEC.md](../SPEC.md) and should be read alongside the comparative analysis in [doc/plugins/ideas-from-opencode.md](./ideas-from-opencode.md).
This is not part of the V1 implementation contract in [doc/SPEC-implementation.md](../SPEC-implementation.md).
It is the full target architecture for the plugin system that should follow V1.
The code in this repo now includes an early plugin runtime and admin UI, but it does not yet deliver the full deployment model described in this spec.
Today, the practical deployment model is:
- single-tenant
- self-hosted
- single-node or otherwise filesystem-persistent
Current limitations to keep in mind:
- Runtime installs assume a writable local filesystem for the plugin package directory and plugin data directory.
- Runtime npm installs assume `npm` is available in the running environment and that the host can reach the configured package registry.
- Published npm packages are the intended install artifact for deployed plugins.
- The repo example plugins under `packages/plugins/examples/` are development conveniences. They work from a source checkout and should not be assumed to exist in a generic published build unless they are explicitly shipped with that build.
- Dynamic plugin install is not yet cloud-ready for horizontally scaled or ephemeral deployments. There is no shared artifact store, install coordination, or cross-node distribution layer yet.
In practice, that means the current implementation is a good fit for local development and self-hosted persistent deployments, but not yet for multi-instance cloud plugin distribution.
Paperclip plugin design is based on the following assumptions:
1. Paperclip is single-tenant and self-hosted.
2. Plugin installation is global to the instance.
3. "Companies" remain core Paperclip business objects, but they are not plugin trust boundaries.
4. Board governance, approval gates, budget hard-stops, and core task invariants remain owned by Paperclip core.
5. Projects already have a real workspace model via `project_workspaces`, and local/runtime plugins should build on that instead of inventing a separate workspace abstraction.
## 3. Goals
The plugin system must:
1. Let operators install global instance-wide plugins.
2. Let plugins add major capabilities without editing Paperclip core.
3. Keep core governance and auditing intact.
4. Support both local/runtime plugins and external SaaS connectors.
5. Support future plugin categories such as:
- new agent adapters
- revenue tracking
- knowledge base
- issue tracker sync
- metrics/dashboards
- file/project tooling
6. Use simple, explicit, typed contracts.
7. Keep failures isolated so one plugin does not crash the entire instance.
## 4. Non-Goals
The first plugin system must not:
1. Allow arbitrary plugins to override core routes or core invariants.
2. Allow arbitrary plugins to mutate approval, auth, issue checkout, or budget enforcement logic.
3. Allow arbitrary third-party plugins to run free-form DB migrations.
4. Depend on project-local plugin folders such as `.paperclip/plugins`.
5. Depend on automatic install-and-execute behavior at server startup from arbitrary config files.
## 5. Terminology
### 5.1 Instance
The single Paperclip deployment an operator installs and controls.
### 5.2 Company
A first-class Paperclip business object inside the instance.
### 5.3 Project Workspace
A workspace attached to a project through `project_workspaces`.
Plugins that need local tooling (file browsing, git, terminals, process tracking) can resolve workspace paths through the project workspace APIs and then operate on the filesystem, spawn processes, and run git commands directly. The host does not wrap these operations — plugins own their own implementations.
This on-disk model is the reason the current implementation expects a persistent writable host filesystem. Cloud-safe artifact replication is future work.
For the current implementation, this install flow should be read as a single-host workflow. A successful install writes packages to the local host, and other app nodes will not automatically receive that plugin unless a future shared distribution mechanism is added.
- UI slot IDs are automatically namespaced by plugin ID (e.g. `@paperclip/plugin-linear:sync-health-widget`), so cross-plugin collisions are structurally impossible
- if a single plugin declares duplicate slot IDs within its own manifest, the host must reject at install time
-`entrypoints.ui` points to the directory containing the built UI bundle
-`ui.slots` declares which extension slots the plugin fills, so the host knows what to mount without loading the bundle eagerly; each slot references an `exportName` from the UI bundle
## 11. Agent Tools
Plugins may contribute tools that Paperclip agents can use during runs.
### 11.1 Tool Declaration
Plugins declare tools in their manifest:
```ts
tools?: Array<{
name: string;
displayName: string;
description: string;
parametersSchema: JsonSchema;
}>;
```
Tool names are automatically namespaced by plugin ID at runtime (e.g. `linear:search-issues`), so plugins cannot shadow core tools or each other's tools.
### 11.2 Tool Execution
When an agent invokes a plugin tool during a run, the host routes the call to the plugin worker via a `executeTool` RPC method:
-`executeTool(input)` — receives tool name, parsed parameters, and run context (agent ID, run ID, company ID, project ID)
The worker executes the tool logic and returns a typed result. The host enforces capability gates — a plugin must declare `agent.tools.register` to contribute tools, and individual tools may require additional capabilities (e.g. `http.outbound` for tools that call external APIs).
By default, plugin tools are available to all agents. The operator may restrict tool availability per agent or per project through plugin configuration.
Plugin tools appear in the agent's tool list alongside core tools but are visually distinguished in the UI as plugin-contributed.
### 11.4 Constraints
- Plugin tools must not override or shadow core tools by name.
- Plugin tools must be idempotent where possible.
- Tool execution is subject to the same timeout and resource limits as other plugin worker calls.
Called when the operator updates the plugin's instance config at runtime.
Input includes:
- new resolved config
If the worker implements this method, it applies the new config without restarting. If the worker does not implement this method, the host restarts the worker process with the new config (graceful shutdown then restart).
The plugin UI calls the host bridge, which forwards the request to the worker. The worker returns typed JSON that the plugin's own frontend components render.
`ctx.data` and `ctx.actions` register handlers that the plugin's own UI calls through the host bridge. `ctx.data.register(key, handler)` backs `usePluginData(key)` on the frontend. `ctx.actions.register(key, handler)` backs `usePluginAction(key)`.
Plugins that need filesystem, git, terminal, or process operations handle those directly using standard Node APIs or libraries. The host provides project workspace metadata through `ctx.projects` so plugins can resolve workspace paths, but the host does not proxy low-level OS operations.
Plugins may provide an optional filter when subscribing to events. The filter is evaluated by the host before dispatching to the worker, so filtered-out events never cross the process boundary.
Supported filter fields:
-`projectId` — only receive events for a specific project
-`companyId` — only receive events for a specific company
-`agentId` — only receive events for a specific agent
Filters are optional. If omitted, the plugin receives all events of the subscribed type. Filters may be combined (e.g. filter by both company and project).
### 16.2 Plugin-to-Plugin Events
Plugins may emit custom events using `ctx.events.emit(name, payload)`. Plugin-emitted events use a namespaced event type: `plugin.<pluginId>.<eventName>`.
Other plugins may subscribe to these events using the same `ctx.events.on()` API:
Plugins ship their own frontend UI as a bundled React module. The host loads plugin UI into designated extension slots and provides a bridge for the plugin frontend to communicate with its own worker backend and with host APIs.
### How Plugin UI Publishing Works In Practice
A plugin's `dist/ui/` directory contains a built React bundle. The host serves this bundle and loads it into the page when the user navigates to a plugin surface (a plugin page, a detail tab, a dashboard widget, etc.).
**The host provides, the plugin renders:**
1. The host defines **extension slots** — designated mount points in the UI where plugin components can appear (pages, tabs, widgets, sidebar entries, action bars).
2. The plugin's UI bundle exports named components for each slot it wants to fill.
3. The host mounts the plugin component into the slot, passing it a **host bridge** object.
4. The plugin component uses the bridge to fetch data from its own worker (via `getData`), call actions (via `performAction`), read host context (current company, project, entity), and use shared host UI primitives (design tokens, common components).
**Concrete example: a Linear plugin ships a dashboard widget.**
The plugin's UI bundle exports:
```tsx
// dist/ui/index.tsx
import { usePluginData, usePluginAction, MetricCard, StatusBadge } from "@paperclipai/plugin-sdk/ui";
export function DashboardWidget({ context }: PluginWidgetProps) {
1. User opens the dashboard. The host sees that the Linear plugin registered a `DashboardWidget` export.
2. The host mounts the plugin's `DashboardWidget` component into the dashboard widget slot, passing `context` (current company, user, etc.) and the bridge.
3.`usePluginData("sync-health", ...)` calls through the bridge → host → plugin worker's `getData` RPC → returns JSON → the plugin component renders it however it wants.
4. When the user clicks "Resync Now", `usePluginAction("resync")` calls through the bridge → host → plugin worker's `performAction` RPC.
**What the host controls:**
- The host decides **where** plugin components appear (which slots exist and when they mount).
- The host provides the **bridge** — plugin UI cannot make arbitrary network requests or access host internals directly.
- The host enforces **capability gates** — if a plugin's worker does not have a capability, the bridge rejects the call even if the UI requests it.
- The host provides **design tokens and shared components** via `@paperclipai/plugin-sdk/ui` so plugins can match the host's visual language without being forced to.
**What the plugin controls:**
- The plugin decides **how** to render its data — it owns its React components, layout, interactions, and state management.
- The plugin decides **what data** to fetch and **what actions** to expose.
- The plugin can use any React patterns (hooks, context, third-party component libraries) inside its bundle.
Plugins are encouraged but not required to use the shared components. A plugin may render entirely custom UI as long as it communicates through the bridge.
### 19.0.2 Bundle Isolation
Plugin UI bundles are loaded as standard ES modules, not iframed. This gives plugins full rendering performance and access to the host's design tokens.
Isolation rules:
- Plugin bundles must not import from host internals. They may only import from `@paperclipai/plugin-sdk/ui` and their own dependencies.
- Plugin bundles must not access `window.fetch` or `XMLHttpRequest` directly for host API calls. All host communication goes through the bridge.
- The host may enforce Content Security Policy rules that restrict plugin network access to the bridge endpoint only.
- Plugin bundles must be statically analyzable — no dynamic `import()` of URLs outside the plugin's own bundle.
If stronger isolation is needed later, the host can move to iframe-based mounting for untrusted plugins without changing the plugin's source code (the bridge API stays the same).
### 19.0.3 Bundle Serving
Plugin UI bundles must be pre-built ESM. The host does not compile or transform plugin UI code at runtime.
The host serves the plugin's `dist/ui/` directory as static assets under a namespaced path:
When the host renders an extension slot, it dynamically imports the plugin's UI entry module from this path, resolves the named export declared in `ui.slots[].exportName`, and mounts it into the slot.
In development, the host may support a `devUiUrl` override in plugin config that points to a local dev server (e.g. Vite) so plugin authors can use hot-reload during development without rebuilding.
The host SDK ships shared components that plugins can import to quickly build UIs that match the host's look and feel. These are convenience building blocks, not a requirement.
Plugins may also use entirely custom components. The shared components exist to reduce boilerplate and keep visual consistency, not to limit what plugins can render.
The `@paperclipai/plugin-sdk/ui` subpath should also export an `ErrorBoundary` component that plugin authors can use to catch rendering errors without crashing the host page.
Each plugin that declares an `instanceConfigSchema` in its manifest gets an auto-generated settings form at `/settings/plugins/:pluginId`. The host renders the form from the JSON Schema.
- text inputs, number inputs, toggles, select dropdowns derived from schema types and enums
- nested objects rendered as fieldsets
- arrays rendered as repeatable field groups with add/remove controls
- secret ref fields: any schema property annotated with `"format": "secret-ref"` renders as a secret picker that resolves through the Paperclip secret provider system rather than a plain text input
- a "Test Connection" action if the plugin declares a `validateConfig` RPC method — the host calls it and displays the result inline
For plugins that need richer settings UX beyond what JSON Schema can express, the plugin may declare a `settingsPage` slot in `ui.slots`. When present, the host renders the plugin's own React component instead of the auto-generated form. The plugin component communicates with its worker through the standard bridge to read and write config.
Both approaches coexist: a plugin can use the auto-generated form for simple config and add a custom settings page slot for advanced configuration or operational dashboards.
The host provides workspace metadata through `ctx.projects` (list workspaces, get primary workspace, resolve workspace from issue or agent/run). Plugins use this metadata to resolve local paths and then operate on the filesystem, spawn processes, shell out to `git`, or open PTY sessions using standard Node APIs or any libraries they choose.
This keeps the host lean — it does not need to maintain a parallel API surface for every OS-level operation a plugin might need. Plugins own their own logic for file browsing, git workflows, terminal sessions, and process management.
When a plugin is uninstalled, the host must handle plugin-owned data explicitly.
### 25.1 Uninstall Process
1. The host sends `shutdown()` to the worker and follows the graceful shutdown policy.
2. The host marks the plugin status `uninstalled` in the `plugins` table (soft delete).
3. Plugin-owned data (`plugin_state`, `plugin_entities`, `plugin_jobs`, `plugin_job_runs`, `plugin_webhook_deliveries`, `plugin_config`) is retained for a configurable grace period (default: 30 days).
4. During the grace period, the operator can reinstall the same plugin and recover its state.
5. After the grace period, the host purges all plugin-owned data for the uninstalled plugin.
6. The operator may force-purge immediately via CLI: `pnpm paperclipai plugin purge <plugin-id>`.
### 25.2 Upgrade Data Considerations
Plugin upgrades do not automatically migrate plugin state. If a plugin's `value_json` shape changes between versions:
- The plugin worker is responsible for migrating its own state on first access after upgrade.
- The host does not run plugin-defined schema migrations.
- Plugins should version their state keys or use a schema version field inside `value_json` to detect and handle format changes.
### 25.3 Upgrade Lifecycle
When upgrading a plugin:
1. The host sends `shutdown()` to the old worker.
2. The host waits for the old worker to drain in-flight work (respecting the shutdown deadline).
3. Any in-flight jobs that do not complete within the deadline are marked `cancelled`.
4. The host installs the new version and starts the new worker.
5. If the new version adds capabilities, the plugin enters `upgrade_pending` and the operator must approve before the new worker becomes `ready`.
### 25.4 Hot Plugin Lifecycle
Plugin install, uninstall, upgrade, and config changes **must** take effect without restarting the Paperclip server. This is a normative requirement, not optional.
The architecture already supports this — plugins run as out-of-process workers with dynamic ESM imports, IPC bridges, and host-managed routing tables. This section makes the requirement explicit so implementations do not regress.
#### 25.4.1 Hot Install
When a plugin is installed at runtime:
1. The host resolves and validates the manifest without stopping existing services.
2. The host spawns a new worker process for the plugin.
3. The host registers the plugin's event subscriptions, job schedules, webhook endpoints, and agent tool declarations in the live routing tables.
4. The host loads the plugin's UI bundle path into the extension slot registry so the frontend can discover it on the next navigation or via a live notification.
5. The plugin enters `ready` status (or `upgrade_pending` if capability approval is required).
No other plugin or host service is interrupted.
#### 25.4.2 Hot Uninstall
When a plugin is uninstalled at runtime:
1. The host sends `shutdown()` and follows the graceful shutdown policy (Section 12.5).
2. The host removes the plugin's event subscriptions, job schedules, webhook endpoints, and agent tool declarations from the live routing tables.
3. The host removes the plugin's UI bundle from the extension slot registry. Any currently mounted plugin UI components are unmounted and replaced with a placeholder or removed entirely.
4. The host marks the plugin `uninstalled` and starts the data retention grace period (Section 25.1).
No server restart is needed.
#### 25.4.3 Hot Upgrade
When a plugin is upgraded at runtime:
1. The host follows the upgrade lifecycle (Section 25.3) — shut down old worker, start new worker.
2. If the new version changes event subscriptions, job schedules, webhook endpoints, or agent tools, the host atomically swaps the old registrations for the new ones.
3. If the new version ships an updated UI bundle, the host invalidates any cached bundle assets and notifies the frontend to reload plugin UI components. Active users see the updated UI on next navigation or via a live refresh notification.
4. If the manifest `apiVersion` is unchanged and no new capabilities are added, the upgrade completes without operator interaction.
#### 25.4.4 Hot Config Change
When an operator updates a plugin's instance config at runtime:
1. The host writes the new config to `plugin_config`.
2. The host sends a `configChanged` notification to the running worker via IPC.
3. The worker receives the new config through `ctx.config` and applies it without restarting. If the plugin needs to re-initialize connections (e.g. a new API token), it does so internally.
4. If the plugin does not handle `configChanged`, the host restarts the worker process with the new config (graceful shutdown then restart).
#### 25.4.5 Frontend Cache Invalidation
The host must version plugin UI bundle URLs (e.g. `/_plugins/:pluginId/ui/:version/*` or content-hash-based paths) so that browser caches do not serve stale bundles after upgrade or reinstall.
The host should emit a `plugin.ui.updated` event that the frontend listens for to trigger re-import of updated plugin modules without a full page reload.
#### 25.4.6 Worker Process Management
The host's plugin process manager must support:
- starting a worker for a newly installed plugin without affecting other workers
- stopping a worker for an uninstalled plugin without affecting other workers
- replacing a worker during upgrade (stop old, start new) atomically from the routing table's perspective
- restarting a worker after crash without operator intervention (with backoff)
Each worker process is independent. There is no shared process pool or batch restart mechanism.
## 26. Plugin Observability
### 26.1 Logging
Plugin workers use `ctx.logger` to emit structured logs. The host captures these logs and stores them in a queryable format.
Log storage rules:
- Plugin logs are stored in a `plugin_logs` table or appended to a log file under the plugin's data directory.
- Each log entry includes: plugin ID, timestamp, level, message, and optional structured metadata.
- Logs are queryable from the plugin settings page in the UI.
- Logs have a configurable retention period (default: 7 days).
- The host captures `stdout` and `stderr` from the worker process as fallback logs even if the worker does not use `ctx.logger`.
### 26.2 Health Dashboard
The plugin settings page must show:
- current worker status (running, error, stopped)
- uptime since last restart
- recent log entries
- job run history with success/failure rates
- webhook delivery history with success/failure rates
- last health check result and diagnostics
- resource usage if available (memory, CPU)
### 26.3 Alerting
The host should emit internal events when plugin health degrades. These use the `plugin.*` namespace (not core domain events) and do not appear in the core activity log:
-`plugin.health.degraded` — worker reporting errors or failing health checks
-`plugin.health.recovered` — worker recovered from error state
-`plugin.worker.crashed` — worker process exited unexpectedly
-`plugin.worker.restarted` — worker restarted after crash
These events can be consumed by other plugins (e.g. a notification plugin) or surfaced in the dashboard.
## 27. Plugin Development And Testing
### 27.1 `@paperclipai/plugin-test-harness`
The host should publish a test harness package that plugin authors use for local development and testing.
The test harness provides:
- a mock host that implements the full SDK interface (`ctx.config`, `ctx.events`, `ctx.state`, etc.)
- ability to send synthetic events and verify handler responses
- ability to trigger job runs and verify side effects
- ability to simulate `getData` and `performAction` calls as if coming from the UI bridge
- ability to simulate `executeTool` calls as if coming from an agent run
- in-memory state and entity stores for assertions
- configurable capability sets for testing capability denial paths
Example usage:
```ts
import { createTestHarness } from "@paperclipai/plugin-test-harness";
A single package simplifies dependency management for plugin authors — one dependency, one version, one changelog. The subpath exports keep bundle separation clean: worker code imports from the root, UI code imports from `/ui`. Build tools tree-shake accordingly so the worker bundle does not include React components and the UI bundle does not include worker-only code.
Versioning rules:
1.**Semver**: The SDK follows strict semantic versioning. Major version bumps indicate breaking changes to either the worker or UI surface; minor versions add new features backwards-compatibly; patch versions are bug fixes only.
2.**Tied to API version**: Each major SDK version corresponds to exactly one plugin `apiVersion`. When `@paperclipai/plugin-sdk@2.x` ships, it targets `apiVersion: 2`. Plugins built with SDK 1.x continue to declare `apiVersion: 1`.
3.**Host multi-version support**: The host must support at least the current and one previous `apiVersion` simultaneously. This means plugins built against the previous SDK major version continue to work without modification. The host maintains separate IPC protocol handlers for each supported API version.
4.**Minimum SDK version in manifest**: Plugins declare `sdkVersion` in the manifest as a semver range (e.g. `">=1.4.0 <2.0.0"`). The host validates this at install time and warns if the plugin's declared range is outside the host's supported SDK versions.
5.**Deprecation timeline**: When a new `apiVersion` ships, the previous version enters a deprecation period of at least 6 months. During this period:
- The host continues to load plugins targeting the deprecated version.
- The host logs a deprecation warning at plugin startup.
- The plugin settings page shows a banner indicating the plugin should be upgraded.
- After the deprecation period ends, the host may drop support for the old version in a future release.
6.**SDK changelog and migration guides**: Each major SDK release must include a migration guide documenting every breaking change, the new API surface, and a step-by-step upgrade path for plugin authors.
7.**UI surface stability**: Breaking changes to shared UI components (removing a component, changing required props) or design tokens require a major version bump just like worker API changes. The single-package model means both surfaces are versioned together, avoiding drift between worker and UI compatibility.
### 29.3 Version Compatibility Matrix
The host should publish a compatibility matrix:
| Host Version | Supported API Versions | SDK Range |
Workspace plugins (file browser, terminal, git, process tracking) do not require additional host APIs — they resolve workspace paths through `ctx.projects` and handle filesystem, git, PTY, and process operations directly.