1739 lines
64 KiB
Markdown
1739 lines
64 KiB
Markdown
# Plugin Ideas From OpenCode
|
|
|
|
Status: design report, not a V1 commitment
|
|
|
|
Paperclip V1 explicitly excludes a plugin framework in [doc/SPEC-implementation.md](../SPEC-implementation.md), but the long-horizon spec says the architecture should leave room for extensions. This report studies the `opencode` plugin system and translates the useful patterns into a Paperclip-shaped design.
|
|
|
|
Assumption for this document: Paperclip is a single-tenant operator-controlled instance. Plugin installation should therefore be global across the instance. "Companies" are still first-class Paperclip objects, but they are organizational records, not tenant-isolation boundaries for plugin trust or installation.
|
|
|
|
## Executive Summary
|
|
|
|
`opencode` has a real plugin system already. It is intentionally low-friction:
|
|
|
|
- plugins are plain JS/TS modules
|
|
- they load from local directories and npm packages
|
|
- they can hook many runtime events
|
|
- they can add custom tools
|
|
- they can extend provider auth flows
|
|
- they run in-process and can mutate runtime behavior directly
|
|
|
|
That model works well for a local coding tool. It should not be copied literally into Paperclip.
|
|
|
|
The main conclusion is:
|
|
|
|
- Paperclip should copy `opencode`'s typed SDK, deterministic loading, low authoring friction, and clear extension surfaces.
|
|
- Paperclip should not copy `opencode`'s trust model, project-local plugin loading, "override by name collision" behavior, or arbitrary in-process mutation hooks for core business logic.
|
|
- Paperclip should use multiple extension classes instead of one generic plugin bag:
|
|
- trusted in-process modules for low-level platform concerns like agent adapters, storage providers, secret providers, and possibly run-log backends
|
|
- out-of-process plugins for most third-party integrations like Linear, GitHub Issues, Grafana, Stripe, and schedulers
|
|
- plugin-contributed agent tools (namespaced, not override-by-collision)
|
|
- plugin-shipped React UI loaded into host extension slots via a typed bridge
|
|
- a typed event bus with server-side filtering and plugin-to-plugin events, plus scheduled jobs for automation
|
|
|
|
If Paperclip does this well, the examples you listed become straightforward:
|
|
|
|
- file browser / terminal / git workflow / child process tracking become workspace plugins that resolve paths from the host and handle OS operations directly
|
|
- Linear / GitHub / Grafana / Stripe become connector plugins
|
|
- future knowledge base and accounting features can also fit the same model
|
|
|
|
## Sources Examined
|
|
|
|
I cloned `anomalyco/opencode` and reviewed commit:
|
|
|
|
- `a965a062595403a8e0083e85770315d5dc9628ab`
|
|
|
|
Primary files reviewed:
|
|
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/plugin/src/index.ts`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/plugin/src/tool.ts`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/opencode/src/plugin/index.ts`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/opencode/src/config/config.ts`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/opencode/src/tool/registry.ts`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/opencode/src/provider/auth.ts`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/web/src/content/docs/plugins.mdx`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/web/src/content/docs/custom-tools.mdx`
|
|
- `https://github.com/anomalyco/opencode/blob/a965a062595403a8e0083e85770315d5dc9628ab/packages/web/src/content/docs/ecosystem.mdx`
|
|
|
|
Relevant Paperclip files reviewed for current extension seams:
|
|
|
|
- [server/src/adapters/registry.ts](../../server/src/adapters/registry.ts)
|
|
- [ui/src/adapters/registry.ts](../../ui/src/adapters/registry.ts)
|
|
- [server/src/storage/provider-registry.ts](../../server/src/storage/provider-registry.ts)
|
|
- [server/src/secrets/provider-registry.ts](../../server/src/secrets/provider-registry.ts)
|
|
- [server/src/services/run-log-store.ts](../../server/src/services/run-log-store.ts)
|
|
- [server/src/services/activity-log.ts](../../server/src/services/activity-log.ts)
|
|
- [doc/SPEC.md](../SPEC.md)
|
|
- [doc/SPEC-implementation.md](../SPEC-implementation.md)
|
|
|
|
## What OpenCode Actually Implements
|
|
|
|
## 1. Plugin authoring API
|
|
|
|
`opencode` exposes a small package, `@opencode-ai/plugin`, with a typed `Plugin` function and a typed `tool()` helper.
|
|
|
|
Core shape:
|
|
|
|
- a plugin is an async function that receives a context object
|
|
- the plugin returns a `Hooks` object
|
|
- hooks are optional
|
|
- plugins can also contribute tools and auth providers
|
|
|
|
The plugin init context includes:
|
|
|
|
- an SDK client
|
|
- current project info
|
|
- current directory
|
|
- current git worktree
|
|
- server URL
|
|
- Bun shell access
|
|
|
|
That is important: `opencode` gives plugins rich runtime power immediately, not a narrow capability API.
|
|
|
|
## 2. Hook model
|
|
|
|
The hook set is broad. It includes:
|
|
|
|
- event subscription
|
|
- config-time hook
|
|
- message hooks
|
|
- model parameter/header hooks
|
|
- permission decision hooks
|
|
- shell env injection
|
|
- tool execution before/after hooks
|
|
- tool definition mutation
|
|
- compaction prompt customization
|
|
- text completion transforms
|
|
|
|
The implementation pattern is very simple:
|
|
|
|
- core code constructs an `output` object
|
|
- each matching plugin hook runs sequentially
|
|
- hooks mutate the `output`
|
|
- final mutated output is used by core
|
|
|
|
This is elegant and easy to extend.
|
|
|
|
It is also extremely powerful. A plugin can change auth headers, model params, permission answers, tool inputs, tool descriptions, and shell environment.
|
|
|
|
## 3. Plugin discovery and load order
|
|
|
|
`opencode` supports two plugin sources:
|
|
|
|
- local files
|
|
- npm packages
|
|
|
|
Local directories:
|
|
|
|
- `~/.config/opencode/plugins/`
|
|
- `.opencode/plugins/`
|
|
|
|
Npm plugins:
|
|
|
|
- listed in config under `plugin: []`
|
|
|
|
Load order is deterministic and documented:
|
|
|
|
1. global config
|
|
2. project config
|
|
3. global plugin directory
|
|
4. project plugin directory
|
|
|
|
Important details:
|
|
|
|
- config arrays are concatenated rather than replaced
|
|
- duplicate plugin names are deduplicated with higher-precedence entries winning
|
|
- internal first-party plugins and default plugins are also loaded through the plugin pipeline
|
|
|
|
This gives `opencode` a real precedence model rather than "whatever loaded last by accident."
|
|
|
|
## 4. Dependency handling
|
|
|
|
For local config/plugin directories, `opencode` will:
|
|
|
|
- ensure a `package.json` exists
|
|
- inject `@opencode-ai/plugin`
|
|
- run `bun install`
|
|
|
|
That lets local plugins and local custom tools import dependencies.
|
|
|
|
This is excellent for local developer ergonomics.
|
|
|
|
It is not a safe default for an operator-controlled control plane server.
|
|
|
|
## 5. Error handling
|
|
|
|
Plugin load failures do not hard-crash the runtime by default.
|
|
|
|
Instead, `opencode`:
|
|
|
|
- logs the error
|
|
- publishes a session error event
|
|
- continues loading other plugins
|
|
|
|
That is a good operational pattern. One bad plugin should not brick the entire product unless the operator has explicitly configured it as required.
|
|
|
|
## 6. Tools are a first-class extension point
|
|
|
|
`opencode` has two ways to add tools:
|
|
|
|
- export tools directly from a plugin via `hook.tool`
|
|
- define local files in `.opencode/tools/` or global tools directories
|
|
|
|
The tool API is strong:
|
|
|
|
- tools have descriptions
|
|
- tools have Zod schemas
|
|
- tool execution gets context like session ID, message ID, directory, and worktree
|
|
- tools are merged into the same registry as built-in tools
|
|
- tool definitions themselves can be mutated by a `tool.definition` hook
|
|
|
|
The most aggressive part of the design:
|
|
|
|
- custom tools can override built-in tools by name
|
|
|
|
That is very powerful for a local coding assistant.
|
|
It is too dangerous for Paperclip core actions.
|
|
|
|
However, the concept of plugins contributing agent-usable tools is very valuable for Paperclip — as long as plugin tools are namespaced (cannot shadow core tools) and capability-gated.
|
|
|
|
## 7. Auth is also a plugin surface
|
|
|
|
`opencode` allows plugins to register auth methods for providers.
|
|
|
|
A plugin can contribute:
|
|
|
|
- auth method metadata
|
|
- prompt flows
|
|
- OAuth flows
|
|
- API key flows
|
|
- request loaders that adapt provider behavior after auth succeeds
|
|
|
|
This is a strong pattern worth copying. Integrations often need custom auth UX and token handling.
|
|
|
|
## 8. Ecosystem evidence
|
|
|
|
The ecosystem page is the best proof that the model is working in practice.
|
|
Community plugins already cover:
|
|
|
|
- sandbox/workspace systems
|
|
- auth providers
|
|
- session headers / telemetry
|
|
- memory/context features
|
|
- scheduling
|
|
- notifications
|
|
- worktree helpers
|
|
- background agents
|
|
- monitoring
|
|
|
|
That validates the main thesis: a simple typed plugin API can create real ecosystem velocity.
|
|
|
|
## What OpenCode Gets Right
|
|
|
|
## 1. Separate plugin SDK from host runtime
|
|
|
|
This is one of the best parts of the design.
|
|
|
|
- plugin authors code against a clean public package
|
|
- host internals can evolve behind the loader
|
|
- runtime code and plugin code have a clean contract boundary
|
|
|
|
Paperclip should absolutely do this.
|
|
|
|
## 2. Deterministic loading and precedence
|
|
|
|
`opencode` is explicit about:
|
|
|
|
- where plugins come from
|
|
- how config merges
|
|
- what order wins
|
|
|
|
Paperclip should copy this discipline.
|
|
|
|
## 3. Low-ceremony authoring
|
|
|
|
A plugin author does not have to learn a giant framework.
|
|
|
|
- export async function
|
|
- return hooks
|
|
- optionally export tools
|
|
|
|
That simplicity matters.
|
|
|
|
## 4. Typed tool definitions
|
|
|
|
The `tool()` helper is excellent:
|
|
|
|
- typed
|
|
- schema-based
|
|
- easy to document
|
|
- easy for runtime validation
|
|
|
|
Paperclip should adopt this style for plugin actions, automations, and UI schemas.
|
|
|
|
## 5. Built-in features and plugins use similar shapes
|
|
|
|
`opencode` uses the same hook system for internal and external plugin-style behavior in several places.
|
|
That reduces special cases.
|
|
|
|
Paperclip can benefit from that with adapters, secret backends, storage providers, and connector modules.
|
|
|
|
## 6. Incremental extension, not giant abstraction upfront
|
|
|
|
`opencode` did not design a giant marketplace platform first.
|
|
It added concrete extension points that real features needed.
|
|
|
|
That is the correct mindset for Paperclip too.
|
|
|
|
## What Paperclip Should Not Copy Directly
|
|
|
|
## 1. In-process arbitrary plugin code as the default
|
|
|
|
`opencode` is basically a local agent runtime, so unsandboxed plugin execution is acceptable for its audience.
|
|
|
|
Paperclip is a control plane for an operator-managed instance with company objects.
|
|
The risk profile is different:
|
|
|
|
- secrets matter
|
|
- approval gates matter
|
|
- budgets matter
|
|
- mutating actions require auditability
|
|
|
|
Default third-party plugins should not run with unrestricted in-process access to server memory, DB handles, and secrets.
|
|
|
|
## 2. Project-local plugin loading
|
|
|
|
`opencode` has project-local plugin folders because the tool is centered around a codebase.
|
|
|
|
Paperclip is not project-scoped. It is instance-scoped.
|
|
The comparable unit is:
|
|
|
|
- instance-installed plugin package
|
|
|
|
Paperclip should not auto-load arbitrary code from a workspace repo like `.paperclip/plugins` or project directories.
|
|
|
|
## 3. Arbitrary mutation hooks on core business decisions
|
|
|
|
Hooks like:
|
|
|
|
- `permission.ask`
|
|
- `tool.execute.before`
|
|
- `chat.headers`
|
|
- `shell.env`
|
|
|
|
make sense in `opencode`.
|
|
|
|
For Paperclip, equivalent hooks into:
|
|
|
|
- approval decisions
|
|
- issue checkout semantics
|
|
- activity log behavior
|
|
- budget enforcement
|
|
|
|
would be a mistake.
|
|
|
|
Core invariants should stay in core code, not become hook-rewritable.
|
|
|
|
## 4. Override-by-name collision
|
|
|
|
Allowing a plugin to replace a built-in tool by name is useful in a local agent product.
|
|
|
|
Paperclip should not allow plugins to silently replace:
|
|
|
|
- core routes
|
|
- core mutating actions
|
|
- auth behaviors
|
|
- permission evaluators
|
|
- budget logic
|
|
- audit logic
|
|
|
|
Extension should be additive or explicitly delegated, never accidental shadowing.
|
|
|
|
## 5. Auto-install and execute from user config
|
|
|
|
`opencode`'s "install dependencies at startup" flow is ergonomic.
|
|
For Paperclip it would be risky because it combines:
|
|
|
|
- package installation
|
|
- code loading
|
|
- execution
|
|
|
|
inside the control-plane server startup path.
|
|
|
|
Paperclip should require an explicit operator install step.
|
|
|
|
## Why Paperclip Needs A Different Shape
|
|
|
|
The products are solving different problems.
|
|
|
|
| Topic | OpenCode | Paperclip |
|
|
|---|---|---|
|
|
| Primary unit | local project/worktree | single-tenant operator instance with company objects |
|
|
| Trust assumption | local power user on own machine | operator managing one trusted Paperclip instance |
|
|
| Failure blast radius | local session/runtime | entire company control plane |
|
|
| Extension style | mutate runtime behavior freely | preserve governance and auditability |
|
|
| UI model | local app can load local behavior | board UI must stay coherent and safe |
|
|
| Security model | host-trusted local plugins | needs capability boundaries and auditability |
|
|
|
|
That means Paperclip should borrow the good ideas from `opencode` but use a stricter architecture.
|
|
|
|
## Paperclip Already Has Useful Pre-Plugin Seams
|
|
|
|
Paperclip has several extension-like seams already:
|
|
|
|
- server adapter registry: [server/src/adapters/registry.ts](../../server/src/adapters/registry.ts)
|
|
- UI adapter registry: [ui/src/adapters/registry.ts](../../ui/src/adapters/registry.ts)
|
|
- storage provider registry: [server/src/storage/provider-registry.ts](../../server/src/storage/provider-registry.ts)
|
|
- secret provider registry: [server/src/secrets/provider-registry.ts](../../server/src/secrets/provider-registry.ts)
|
|
- pluggable run-log store seam: [server/src/services/run-log-store.ts](../../server/src/services/run-log-store.ts)
|
|
- activity log and live event emission: [server/src/services/activity-log.ts](../../server/src/services/activity-log.ts)
|
|
|
|
This is good news.
|
|
Paperclip does not need to invent extensibility from scratch.
|
|
It needs to unify and harden existing seams.
|
|
|
|
## Recommended Paperclip Plugin Model
|
|
|
|
## 1. Use multiple extension classes
|
|
|
|
Do not create one giant `hooks` object for everything.
|
|
|
|
Use distinct plugin classes with different trust models.
|
|
|
|
| Extension class | Examples | Runtime model | Trust level | Why |
|
|
|---|---|---|---|---|
|
|
| Platform module | agent adapters, storage providers, secret providers, run-log backends | in-process | highly trusted | tight integration, performance, low-level APIs |
|
|
| Connector plugin | Linear, GitHub Issues, Grafana, Stripe | out-of-process worker or sidecar | medium | external sync, safer isolation, clearer failure boundary |
|
|
| Workspace plugin | file browser, terminal, git workflow, child process/server tracking | out-of-process, direct OS access | medium | resolves workspace paths from host, owns filesystem/git/PTY/process logic directly |
|
|
| UI contribution | dashboard widgets, settings forms, company panels | plugin-shipped React bundles in host extension slots via bridge | medium | plugins own their rendering; host controls slot placement and bridge access |
|
|
| Automation plugin | alerts, schedulers, sync jobs, webhook processors | out-of-process | medium | event-driven automation is a natural plugin fit |
|
|
|
|
This split is the most important design recommendation in this report.
|
|
|
|
## 2. Keep low-level modules separate from third-party plugins
|
|
|
|
Paperclip already has this pattern implicitly:
|
|
|
|
- adapters are one thing
|
|
- storage providers are another
|
|
- secret providers are another
|
|
|
|
Keep that separation.
|
|
|
|
I would formalize it like this:
|
|
|
|
- `module` means trusted code loaded by the host for low-level runtime services
|
|
- `plugin` means integration code that talks to Paperclip through a typed plugin protocol and capability model
|
|
|
|
This avoids trying to force Stripe, a PTY terminal, and a new agent adapter into the same abstraction.
|
|
|
|
## 3. Prefer event-driven extensions over core-logic mutation
|
|
|
|
For third-party plugins, the primary API should be:
|
|
|
|
- subscribe to typed domain events (with optional server-side filtering)
|
|
- emit plugin-namespaced events for cross-plugin communication
|
|
- read instance state, including company-bound business records when relevant
|
|
- register webhooks
|
|
- run scheduled jobs
|
|
- contribute tools that agents can use during runs
|
|
- write plugin-owned state
|
|
- add additive UI surfaces
|
|
- invoke explicit Paperclip actions through the API
|
|
|
|
Do not make third-party plugins responsible for:
|
|
|
|
- deciding whether an approval passes
|
|
- intercepting issue checkout semantics
|
|
- rewriting activity log behavior
|
|
- overriding budget hard-stops
|
|
|
|
Those are core invariants.
|
|
|
|
## 4. Plugins ship their own UI
|
|
|
|
Plugins ship their own React UI as a bundled module inside `dist/ui/`. The host loads plugin components into designated **extension slots** (pages, tabs, widgets, sidebar entries) and provides a **bridge** for the plugin frontend to talk to its own worker backend and to access host context.
|
|
|
|
**How it works:**
|
|
|
|
1. The plugin's UI exports named components for each slot it fills (e.g. `DashboardWidget`, `IssueDetailTab`, `SettingsPage`).
|
|
2. The host mounts the plugin component into the correct slot, passing a bridge object with hooks like `usePluginData(key, params)` and `usePluginAction(key)`.
|
|
3. The plugin component fetches data from its own worker via the bridge and renders it however it wants.
|
|
4. The host enforces capability gates through the bridge — if the worker doesn't have a capability, the bridge rejects the call.
|
|
|
|
**What the host controls:** where plugin components appear, the bridge API, capability enforcement, and shared UI primitives (`@paperclipai/plugin-sdk/ui`) with design tokens and common components.
|
|
|
|
**What the plugin controls:** how to render its data, what data to fetch, what actions to expose, and whether to use the host's shared components or build entirely custom UI.
|
|
|
|
First version extension slots:
|
|
|
|
- dashboard widgets
|
|
- settings pages
|
|
- detail-page tabs (project, issue, agent, goal, run)
|
|
- sidebar entries
|
|
- company-context plugin pages
|
|
|
|
The host SDK ships shared components (MetricCard, DataTable, StatusBadge, LogView, etc.) for visual consistency, but these are optional.
|
|
|
|
Later, if untrusted third-party plugins become common, the host can move to iframe-based isolation without changing the plugin's source code (the bridge API stays the same).
|
|
|
|
## 5. Make installation global and keep mappings/config separate
|
|
|
|
`opencode` is mostly user-level local config.
|
|
Paperclip should treat plugin installation as a global instance-level action.
|
|
|
|
Examples:
|
|
|
|
- install `@paperclip/plugin-linear` once
|
|
- make it available everywhere immediately
|
|
- optionally store mappings over Paperclip objects if one company maps to a different Linear team than another
|
|
|
|
## 6. Use project workspaces as the primary anchor for local tooling
|
|
|
|
Paperclip already has a concrete workspace model for projects:
|
|
|
|
- projects expose `workspaces` and `primaryWorkspace`
|
|
- the database already has `project_workspaces`
|
|
- project routes already support creating, updating, and deleting workspaces
|
|
- heartbeat resolution already prefers project workspaces before falling back to task-session or agent-home workspaces
|
|
|
|
That means local/runtime plugins should generally anchor themselves to projects first, not invent a parallel workspace model.
|
|
|
|
Practical guidance:
|
|
|
|
- file browser should browse project workspaces first
|
|
- terminal sessions should be launchable from a project workspace
|
|
- git should treat the project workspace as the repo root anchor
|
|
- dev server and child-process tracking should attach to project workspaces
|
|
- issue and agent views can still deep-link into the relevant project workspace context
|
|
|
|
In other words:
|
|
|
|
- `project` is the business object
|
|
- `project_workspace` is the local runtime anchor
|
|
- plugins should build on that instead of creating an unrelated workspace model first
|
|
|
|
## 7. Let plugins contribute agent tools
|
|
|
|
`opencode` makes tools a first-class extension point. This is one of the highest-value surfaces for Paperclip too.
|
|
|
|
A Linear plugin should be able to contribute a `search-linear-issues` tool that agents use during runs. A git plugin should contribute `create-branch` and `get-diff`. A file browser plugin should contribute `read-file` and `list-directory`.
|
|
|
|
The key constraints:
|
|
|
|
- plugin tools are namespaced by plugin ID (e.g. `linear:search-issues`) so they cannot shadow core tools
|
|
- plugin tools require the `agent.tools.register` capability
|
|
- tool execution goes through the same worker RPC boundary as everything else
|
|
- tool results appear in run logs
|
|
|
|
This is a natural fit — the plugin already has the SDK context, the external API credentials, and the domain logic. Wrapping that in a tool definition is minimal additional work for the plugin author.
|
|
|
|
## 8. Support plugin-to-plugin events
|
|
|
|
Plugins should be able to emit custom events that other plugins can subscribe to. For example, the git plugin detects a push and emits `plugin.@paperclip/plugin-git.push-detected`. The GitHub Issues plugin subscribes to that event and updates PR links.
|
|
|
|
This avoids plugins needing to coordinate through shared state or external channels. The host routes plugin events through the same event bus with the same delivery semantics as core events.
|
|
|
|
Plugin events use a `plugin.<pluginId>.*` namespace so they cannot collide with core events.
|
|
|
|
## 9. Auto-generate settings UI from config schema
|
|
|
|
Plugins that declare an `instanceConfigSchema` should get an auto-generated settings form for free. The host renders text inputs, dropdowns, toggles, arrays, and secret-ref pickers directly from the JSON Schema.
|
|
|
|
For plugins that need richer settings UX, they can declare a `settingsPage` extension slot and ship a custom React component. Both approaches coexist.
|
|
|
|
This matters because settings forms are boilerplate that every plugin needs. Auto-generating them from the schema that already exists removes a significant chunk of authoring friction.
|
|
|
|
## 10. Design for graceful shutdown and upgrade
|
|
|
|
The spec should be explicit about what happens when a plugin worker stops — during upgrades, uninstalls, or instance restarts.
|
|
|
|
The recommended policy:
|
|
|
|
- send `shutdown()` with a configurable deadline (default 10 seconds)
|
|
- SIGTERM after deadline, SIGKILL after 5 more seconds
|
|
- in-flight jobs marked `cancelled`
|
|
- in-flight bridge calls return structured errors to the UI
|
|
|
|
For upgrades specifically: the old worker drains, the new worker starts. If the new version adds capabilities, it enters `upgrade_pending` until the operator approves.
|
|
|
|
## 11. Define uninstall data lifecycle
|
|
|
|
When a plugin is uninstalled, its data (`plugin_state`, `plugin_entities`, `plugin_jobs`, etc.) should be retained for a grace period (default 30 days), not immediately deleted. The operator can reinstall within the grace period and recover state, or force-purge via CLI.
|
|
|
|
This matters because accidental uninstalls should not cause irreversible data loss.
|
|
|
|
## 12. Invest in plugin observability
|
|
|
|
Plugin logs via `ctx.logger` should be stored and queryable from the plugin settings page. The host should also capture raw `stdout`/`stderr` from the worker process as fallback.
|
|
|
|
The plugin health dashboard should show: worker status, uptime, recent logs, job success/failure rates, webhook delivery rates, and resource usage. The host should emit internal events (`plugin.health.degraded`, `plugin.worker.crashed`) that other plugins or dashboards can consume.
|
|
|
|
This is critical for operators. Without observability, debugging plugin issues requires SSH access and manual log tailing.
|
|
|
|
## 13. Ship a test harness and starter template
|
|
|
|
A `@paperclipai/plugin-test-harness` package should provide a mock host with in-memory stores, synthetic event emission, and `getData`/`performAction`/`executeTool` simulation. Plugin authors should be able to write unit tests without a running Paperclip instance.
|
|
|
|
A `create-paperclip-plugin` CLI should scaffold a working plugin with manifest, worker, UI bundle, test file, and build config.
|
|
|
|
Low authoring friction was called out as one of `opencode`'s best qualities. The test harness and starter template are how Paperclip achieves the same.
|
|
|
|
## 14. Support hot plugin lifecycle
|
|
|
|
Plugin install, uninstall, upgrade, and config changes should take effect without restarting the Paperclip server. This is critical for developer workflow and operator experience.
|
|
|
|
The out-of-process worker architecture makes this natural:
|
|
|
|
- **Hot install**: spawn a new worker, register its event subscriptions, job schedules, webhook endpoints, and agent tools in live routing tables, load its UI bundle into the extension slot registry.
|
|
- **Hot uninstall**: graceful shutdown of the worker, remove all registrations from routing tables, unmount UI components, start data retention grace period.
|
|
- **Hot upgrade**: shut down old worker, start new worker, atomically swap routing table entries, invalidate UI bundle cache so the frontend loads the updated bundle.
|
|
- **Hot config change**: write new config to `plugin_config`, notify the running worker via IPC (`configChanged`). The worker applies the change without restarting. If it doesn't handle `configChanged`, the host restarts just that worker.
|
|
|
|
Frontend cache invalidation uses versioned or content-hashed bundle URLs and a `plugin.ui.updated` event that triggers re-import without a full page reload.
|
|
|
|
Each worker process is independent — starting, stopping, or replacing one worker never affects any other plugin or the host itself.
|
|
|
|
## 15. Define SDK versioning and compatibility
|
|
|
|
`opencode` does not have a formal SDK versioning story because plugins run in-process and are effectively pinned to the current runtime. Paperclip's out-of-process model means plugins may be built against one SDK version and run on a host that has moved forward. This needs explicit rules.
|
|
|
|
Recommended approach:
|
|
|
|
- **Single SDK package**: `@paperclipai/plugin-sdk` with subpath exports — root for worker code, `/ui` for frontend code. One dependency, one version, one changelog.
|
|
- **SDK major version = API version**: `@paperclipai/plugin-sdk@2.x` targets `apiVersion: 2`. Plugins built with SDK 1.x declare `apiVersion: 1` and continue to work.
|
|
- **Host multi-version support**: The host supports at least the current and one previous `apiVersion` simultaneously with separate IPC protocol handlers per version.
|
|
- **`sdkVersion` in manifest**: Plugins declare a semver range (e.g. `">=1.4.0 <2.0.0"`). The host validates this at install time.
|
|
- **Deprecation timeline**: Previous API versions get at least 6 months of continued support after a new version ships. The host logs deprecation warnings and shows a banner on the plugin settings page.
|
|
- **Migration guides**: Each major SDK release ships with a step-by-step migration guide covering every breaking change.
|
|
- **UI surface versioned with worker**: Both worker and UI surfaces are in the same package, so they version together. Breaking changes to shared UI components require a major version bump just like worker API changes.
|
|
- **Published compatibility matrix**: The host publishes a matrix of supported API versions and SDK ranges, queryable via API.
|
|
|
|
## A Concrete SDK Shape For Paperclip
|
|
|
|
An intentionally narrow first pass could look like this:
|
|
|
|
```ts
|
|
import { definePlugin, z } from "@paperclipai/plugin-sdk";
|
|
|
|
export default definePlugin({
|
|
id: "@paperclip/plugin-linear",
|
|
version: "0.1.0",
|
|
categories: ["connector", "ui"],
|
|
capabilities: [
|
|
"events.subscribe",
|
|
"jobs.schedule",
|
|
"http.outbound",
|
|
"instance.settings.register",
|
|
"ui.dashboardWidget.register",
|
|
"secrets.read-ref",
|
|
],
|
|
instanceConfigSchema: z.object({
|
|
linearBaseUrl: z.string().url().optional(),
|
|
companyMappings: z.array(
|
|
z.object({
|
|
companyId: z.string(),
|
|
teamId: z.string(),
|
|
apiTokenSecretRef: z.string(),
|
|
}),
|
|
).default([]),
|
|
}),
|
|
async register(ctx) {
|
|
ctx.jobs.register("linear-pull", { cron: "*/5 * * * *" }, async (job) => {
|
|
// sync Linear issues into plugin-owned state or explicit Paperclip entities
|
|
});
|
|
|
|
// subscribe with optional server-side filter
|
|
ctx.events.on("issue.created", { projectId: "proj-1" }, async (event) => {
|
|
// only receives issue.created events for project proj-1
|
|
});
|
|
|
|
// subscribe to events from another plugin
|
|
ctx.events.on("plugin.@paperclip/plugin-git.push-detected", async (event) => {
|
|
// react to the git plugin detecting a push
|
|
});
|
|
|
|
// contribute a tool that agents can use during runs
|
|
ctx.tools.register("search-linear-issues", {
|
|
displayName: "Search Linear Issues",
|
|
description: "Search for Linear issues by query",
|
|
parametersSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] },
|
|
}, async (params, runCtx) => {
|
|
// search Linear API and return results
|
|
return { content: JSON.stringify(results) };
|
|
});
|
|
|
|
// getData is called by the plugin's own UI components via the host bridge
|
|
ctx.data.register("sync-health", async ({ companyId }) => {
|
|
// return typed JSON that the plugin's DashboardWidget component renders
|
|
return { syncedCount: 142, trend: "+12 today", mappings: [...] };
|
|
});
|
|
|
|
ctx.actions.register("resync", async ({ companyId }) => {
|
|
// run sync logic
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
The plugin's UI bundle (separate from the worker) might look like:
|
|
|
|
```tsx
|
|
// dist/ui/index.tsx
|
|
import { usePluginData, usePluginAction, MetricCard, ErrorBoundary } from "@paperclipai/plugin-sdk/ui";
|
|
|
|
export function DashboardWidget({ context }: PluginWidgetProps) {
|
|
const { data, loading, error } = usePluginData("sync-health", { companyId: context.companyId });
|
|
const resync = usePluginAction("resync");
|
|
|
|
if (loading) return <Spinner />;
|
|
if (error) return <div>Plugin error: {error.message} ({error.code})</div>;
|
|
|
|
return (
|
|
<ErrorBoundary fallback={<div>Widget failed to render</div>}>
|
|
<MetricCard label="Synced Issues" value={data.syncedCount} trend={data.trend} />
|
|
<button onClick={() => resync({ companyId: context.companyId })}>Resync Now</button>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
```
|
|
|
|
The important point is not the exact syntax.
|
|
The important point is the contract shape:
|
|
|
|
- typed manifest
|
|
- explicit capabilities
|
|
- explicit global config with optional company mappings
|
|
- event subscriptions with optional server-side filtering
|
|
- plugin-to-plugin events via namespaced event types
|
|
- agent tool contributions
|
|
- jobs
|
|
- plugin-shipped UI that communicates with its worker through the host bridge
|
|
- structured error propagation from worker to UI
|
|
|
|
## Recommended Core Extension Surfaces
|
|
|
|
## 1. Platform module surfaces
|
|
|
|
These should stay close to the current registry style.
|
|
|
|
Candidates:
|
|
|
|
- `registerAgentAdapter()`
|
|
- `registerStorageProvider()`
|
|
- `registerSecretProvider()`
|
|
- `registerRunLogStore()`
|
|
|
|
These are trusted platform modules, not casual plugins.
|
|
|
|
## 2. Connector plugin surfaces
|
|
|
|
These are the best near-term plugin candidates.
|
|
|
|
Capabilities:
|
|
|
|
- subscribe to domain events
|
|
- define scheduled sync jobs
|
|
- expose plugin-specific API routes under `/api/plugins/:pluginId/...`
|
|
- use company secret refs
|
|
- write plugin state
|
|
- publish dashboard data
|
|
- log activity through core APIs
|
|
|
|
Examples:
|
|
|
|
- Linear issue sync
|
|
- GitHub issue sync
|
|
- Grafana dashboard cards
|
|
- Stripe MRR / subscription rollups
|
|
|
|
## 3. Workspace-runtime surfaces
|
|
|
|
Workspace plugins handle local tooling directly:
|
|
|
|
- file browser
|
|
- terminal
|
|
- git workflow
|
|
- child process tracking
|
|
- local dev server tracking
|
|
|
|
Plugins resolve workspace paths through host APIs (`ctx.projects` provides workspace metadata including `cwd`, `repoUrl`, etc.) 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.
|
|
|
|
The host does not wrap or proxy these operations. This keeps the core lean — no need to maintain a parallel API surface for every OS-level operation a plugin might need. Plugins own their own implementations.
|
|
|
|
## Governance And Safety Requirements
|
|
|
|
Any Paperclip plugin system has to preserve core control-plane invariants from the repo docs.
|
|
|
|
That means:
|
|
|
|
- plugin install is global to the instance
|
|
- "companies" remain business objects in the API and data model, not tenant boundaries
|
|
- approval gates remain core-owned
|
|
- budget hard-stops remain core-owned
|
|
- mutating actions are activity-logged
|
|
- secrets remain ref-based and redacted in logs
|
|
|
|
I would require the following for every plugin:
|
|
|
|
## 1. Capability declaration
|
|
|
|
Every plugin declares a static capability set such as:
|
|
|
|
- `companies.read`
|
|
- `issues.read`
|
|
- `issues.write`
|
|
- `events.subscribe`
|
|
- `events.emit`
|
|
- `jobs.schedule`
|
|
- `http.outbound`
|
|
- `webhooks.receive`
|
|
- `assets.read`
|
|
- `assets.write`
|
|
- `secrets.read-ref`
|
|
- `agent.tools.register`
|
|
- `plugin.state.read`
|
|
- `plugin.state.write`
|
|
|
|
The board/operator sees this before installation.
|
|
|
|
## 2. Global installation
|
|
|
|
A plugin is installed once and becomes available across the instance.
|
|
If it needs mappings over specific Paperclip objects, those are plugin data, not enable/disable boundaries.
|
|
|
|
## 3. Activity logging
|
|
|
|
Plugin-originated mutations should flow through the same activity log mechanism, with a dedicated `plugin` actor type:
|
|
|
|
- `actor_type = plugin`
|
|
- `actor_id = <plugin-id>` (e.g. `@paperclip/plugin-linear`)
|
|
|
|
## 4. Health and failure reporting
|
|
|
|
Each plugin should expose:
|
|
|
|
- enabled/disabled state
|
|
- last successful run
|
|
- last error
|
|
- recent webhook/job history
|
|
|
|
One broken plugin must not break the rest of the company.
|
|
|
|
## 5. Secret handling
|
|
|
|
Plugins should receive secret refs, not raw secret values in config persistence.
|
|
Resolution should go through the existing secret provider abstraction.
|
|
|
|
## 6. Resource limits
|
|
|
|
Plugins should have:
|
|
|
|
- timeout limits
|
|
- concurrency limits
|
|
- retry policies
|
|
- optional per-plugin budgets
|
|
|
|
This matters especially for sync connectors and workspace plugins.
|
|
|
|
## Data Model Additions To Consider
|
|
|
|
I would avoid "arbitrary third-party plugin-defined SQL migrations" in the first version.
|
|
That is too much power too early.
|
|
|
|
The right mental model is:
|
|
|
|
- reuse core tables when the data is clearly part of Paperclip itself
|
|
- use generic extension tables for most plugin-owned state
|
|
- only allow plugin-specific tables later, and only for trusted platform modules or a tightly controlled migration workflow
|
|
|
|
## Recommended Postgres Strategy For Extensions
|
|
|
|
### 1. Core tables stay core
|
|
|
|
If a concept is becoming part of Paperclip's actual product model, it should get a normal first-party table.
|
|
|
|
Examples:
|
|
|
|
- `project_workspaces` is already a core table because project workspaces are now part of Paperclip itself
|
|
- if a future "project git state" becomes a core feature rather than plugin-owned metadata, that should also be a first-party table
|
|
|
|
### 2. Most plugins should start in generic extension tables
|
|
|
|
For most plugins, the host should provide a few generic persistence tables and the plugin stores namespaced records there.
|
|
|
|
This keeps the system manageable:
|
|
|
|
- simpler migrations
|
|
- simpler backup/restore
|
|
- simpler portability story
|
|
- easier operator review
|
|
- fewer chances for plugin schema drift to break the instance
|
|
|
|
### 3. Scope plugin data to Paperclip objects before adding custom schemas
|
|
|
|
A lot of plugin data naturally hangs off existing Paperclip objects:
|
|
|
|
- project workspace plugin state should often scope to `project` or `project_workspace`
|
|
- issue sync state should scope to `issue`
|
|
- metrics widgets may scope to `company`, `project`, or `goal`
|
|
- process tracking may scope to `project_workspace`, `agent`, or `run`
|
|
|
|
That gives a good default keying model before introducing custom tables.
|
|
|
|
### 4. Add trusted module migrations later, not arbitrary plugin migrations now
|
|
|
|
If Paperclip eventually needs extension-owned tables, I would only allow that for:
|
|
|
|
- trusted first-party packages
|
|
- trusted platform modules
|
|
- maybe explicitly installed admin-reviewed plugins with pinned versions
|
|
|
|
I would not let random third-party plugins run free-form schema migrations on startup.
|
|
|
|
Instead, add a controlled mechanism later if it becomes necessary.
|
|
|
|
## Suggested baseline extension tables
|
|
|
|
## 1. `plugins`
|
|
|
|
Instance-level installation record.
|
|
|
|
Suggested fields:
|
|
|
|
- `id`
|
|
- `package_name`
|
|
- `version`
|
|
- `categories`
|
|
- `manifest_json`
|
|
- `installed_at`
|
|
- `status`
|
|
|
|
## 2. `plugin_config`
|
|
|
|
Instance-level plugin config.
|
|
|
|
Suggested fields:
|
|
|
|
- `id`
|
|
- `plugin_id`
|
|
- `config_json`
|
|
- `created_at`
|
|
- `updated_at`
|
|
- `last_error`
|
|
|
|
## 3. `plugin_state`
|
|
|
|
Generic key/value state for plugins.
|
|
|
|
Suggested fields:
|
|
|
|
- `id`
|
|
- `plugin_id`
|
|
- `scope_kind` (`instance | company | project | project_workspace | agent | issue | goal | run`)
|
|
- `scope_id` nullable
|
|
- `namespace`
|
|
- `state_key`
|
|
- `value_json`
|
|
- `updated_at`
|
|
|
|
This is enough for many connectors before allowing custom tables.
|
|
|
|
Examples:
|
|
|
|
- Linear external IDs keyed by `issue`
|
|
- GitHub sync cursors keyed by `project`
|
|
- file browser preferences keyed by `project_workspace`
|
|
- git branch metadata keyed by `project_workspace`
|
|
- process metadata keyed by `project_workspace` or `run`
|
|
|
|
## 4. `plugin_jobs`
|
|
|
|
Scheduled job and run tracking.
|
|
|
|
Suggested fields:
|
|
|
|
- `id`
|
|
- `plugin_id`
|
|
- `scope_kind` nullable
|
|
- `scope_id` nullable
|
|
- `job_key`
|
|
- `status`
|
|
- `last_started_at`
|
|
- `last_finished_at`
|
|
- `last_error`
|
|
|
|
## 5. `plugin_webhook_deliveries`
|
|
|
|
If plugins expose webhooks, delivery history is worth storing.
|
|
|
|
Suggested fields:
|
|
|
|
- `id`
|
|
- `plugin_id`
|
|
- `scope_kind` nullable
|
|
- `scope_id` nullable
|
|
- `endpoint_key`
|
|
- `status`
|
|
- `received_at`
|
|
- `response_code`
|
|
- `error`
|
|
|
|
## 6. Maybe later: `plugin_entities`
|
|
|
|
If generic plugin state becomes too limiting, add a structured, queryable entity table for connector records before allowing arbitrary plugin migrations.
|
|
|
|
Suggested fields:
|
|
|
|
- `id`
|
|
- `plugin_id`
|
|
- `entity_type`
|
|
- `scope_kind`
|
|
- `scope_id`
|
|
- `external_id`
|
|
- `title`
|
|
- `status`
|
|
- `data_json`
|
|
- `updated_at`
|
|
|
|
This is a useful middle ground:
|
|
|
|
- much more queryable than opaque key/value state
|
|
- still avoids letting every plugin create its own relational schema immediately
|
|
|
|
## How The Requested Examples Map To This Model
|
|
|
|
| Use case | Best fit | Host primitives needed | Notes |
|
|
|---|---|---|---|
|
|
| File browser | workspace plugin | project workspace metadata | plugin owns filesystem ops directly |
|
|
| Terminal | workspace plugin | project workspace metadata | plugin spawns PTY sessions directly |
|
|
| Git workflow | workspace plugin | project workspace metadata | plugin shells out to git directly |
|
|
| Linear issue tracking | connector plugin | jobs, webhooks, secret refs, issue sync API | very strong plugin candidate |
|
|
| GitHub issue tracking | connector plugin | jobs, webhooks, secret refs | very strong plugin candidate |
|
|
| Grafana metrics | connector plugin + dashboard widget | outbound HTTP | probably read-only first |
|
|
| Child process/server tracking | workspace plugin | project workspace metadata | plugin manages processes directly |
|
|
| Stripe revenue tracking | connector plugin | secret refs, scheduled sync, company metrics API | strong plugin candidate |
|
|
|
|
# Plugin Examples
|
|
|
|
## Workspace File Browser
|
|
|
|
Package idea: `@paperclip/plugin-workspace-files`
|
|
|
|
This plugin lets the board inspect project workspaces, agent workspaces, generated artifacts, and issue-related files without dropping to the shell. It is useful for:
|
|
|
|
- browsing files inside project workspaces
|
|
- debugging what an agent changed
|
|
- reviewing generated outputs before approval
|
|
- attaching files from a workspace to issues
|
|
- understanding repo layout for a company
|
|
- inspecting agent home workspaces in local-trusted mode
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/workspace-files`
|
|
- Main page: `/:companyPrefix/plugins/workspace-files`
|
|
- Project tab: `/:companyPrefix/projects/:projectId?tab=files`
|
|
- Optional issue tab: `/:companyPrefix/issues/:issueId?tab=files`
|
|
- Optional agent tab: `/:companyPrefix/agents/:agentId?tab=workspace`
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- choose whether the plugin defaults to `project.primaryWorkspace`
|
|
- choose which project workspaces are visible
|
|
- choose whether file writes are allowed or read-only
|
|
- choose whether hidden files are visible
|
|
- Main explorer page:
|
|
- project picker at the top
|
|
- workspace picker scoped to the selected project's `workspaces`
|
|
- tree view on the left
|
|
- file preview pane on the right
|
|
- search box for filename/path search
|
|
- actions: copy path, download file, attach to issue, open diff
|
|
- Project tab:
|
|
- opens directly into the project's primary workspace
|
|
- lets the board switch among all project workspaces
|
|
- shows workspace metadata like `cwd`, `repoUrl`, and `repoRef`
|
|
- Issue tab:
|
|
- resolves the issue's project and opens that project's workspace context
|
|
- shows files linked to the issue
|
|
- lets the board pull files from the project workspace into issue attachments
|
|
- shows the path and last modified info for each linked file
|
|
- Agent tab:
|
|
- shows the agent's current resolved workspace
|
|
- if the run is attached to a project, links back to the project workspace view
|
|
- lets the board inspect files the agent is currently touching
|
|
|
|
Core workflows:
|
|
|
|
- Board opens a project and browses its primary workspace files.
|
|
- Board switches from one project workspace to another when a project has multiple checkouts or repo references.
|
|
- Board opens an issue, attaches a generated artifact from the file browser, and leaves a review comment.
|
|
- Board opens an agent detail page to inspect the exact files behind a failing run.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.sidebar.register`
|
|
- `ui.page.register`
|
|
- `ui.detailTab.register` for `project`, `issue`, and `agent`
|
|
- `projects.read`
|
|
- `project.workspaces.read`
|
|
- optional `assets.write`
|
|
- `activity.log.write`
|
|
|
|
The plugin resolves workspace paths through `ctx.projects` and handles all filesystem operations (read, write, stat, search, list directory) directly using Node APIs.
|
|
|
|
Optional event subscriptions:
|
|
|
|
- `events.subscribe(agent.run.started)`
|
|
- `events.subscribe(agent.run.finished)`
|
|
- `events.subscribe(issue.attachment.created)`
|
|
|
|
## Workspace Terminal
|
|
|
|
Package idea: `@paperclip/plugin-terminal`
|
|
|
|
This plugin gives the board a controlled terminal UI for project workspaces and agent workspaces. It is useful for:
|
|
|
|
- debugging stuck runs
|
|
- verifying environment state
|
|
- running targeted manual commands
|
|
- watching long-running commands
|
|
- pairing a human operator with an agent workflow
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/terminal`
|
|
- Main page: `/:companyPrefix/plugins/terminal`
|
|
- Project tab: `/:companyPrefix/projects/:projectId?tab=terminal`
|
|
- Optional agent tab: `/:companyPrefix/agents/:agentId?tab=terminal`
|
|
- Optional run tab: `/:companyPrefix/agents/:agentId/runs/:runId?tab=terminal`
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- allowed shells and shell policy
|
|
- whether commands are read-only, free-form, or allow-listed
|
|
- whether terminals require an explicit operator confirmation before launch
|
|
- whether new terminal sessions default to the project's primary workspace
|
|
- Terminal home page:
|
|
- list of active terminal sessions
|
|
- button to open a new session
|
|
- project picker, then workspace picker from that project's workspaces
|
|
- optional agent association
|
|
- terminal panel with input, resize, and reconnect support
|
|
- controls: interrupt, kill, clear, save transcript
|
|
- Project terminal tab:
|
|
- opens a session already scoped to the project's primary workspace
|
|
- lets the board switch among the project's configured workspaces
|
|
- shows recent commands and related process/server state for that project
|
|
- Agent terminal tab:
|
|
- opens a session already scoped to the agent's workspace
|
|
- shows recent related runs and commands
|
|
- Run terminal tab:
|
|
- lets the board inspect the environment around a specific failed run
|
|
|
|
Core workflows:
|
|
|
|
- Board opens a terminal against an agent workspace to reproduce a failing command.
|
|
- Board opens a project page and launches a terminal directly in that project's primary workspace.
|
|
- Board watches a long-running dev server or test command from the terminal page.
|
|
- Board kills or interrupts a runaway process from the same UI.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.sidebar.register`
|
|
- `ui.page.register`
|
|
- `ui.detailTab.register` for `project`, `agent`, and `run`
|
|
- `projects.read`
|
|
- `project.workspaces.read`
|
|
- `activity.log.write`
|
|
|
|
The plugin resolves workspace paths through `ctx.projects` and handles PTY session management (open, input, resize, terminate, subscribe) directly using Node PTY libraries.
|
|
|
|
Optional event subscriptions:
|
|
|
|
- `events.subscribe(agent.run.started)`
|
|
- `events.subscribe(agent.run.failed)`
|
|
- `events.subscribe(agent.run.cancelled)`
|
|
|
|
## Git Workflow
|
|
|
|
Package idea: `@paperclip/plugin-git`
|
|
|
|
This plugin adds repo-aware workflow tooling around issues and workspaces. It is useful for:
|
|
|
|
- branch creation tied to issues
|
|
- quick diff review
|
|
- commit and worktree visibility
|
|
- PR preparation
|
|
- treating the project's primary workspace as the canonical repo anchor
|
|
- seeing whether an agent's workspace is clean or dirty
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/git`
|
|
- Main page: `/:companyPrefix/plugins/git`
|
|
- Project tab: `/:companyPrefix/projects/:projectId?tab=git`
|
|
- Optional issue tab: `/:companyPrefix/issues/:issueId?tab=git`
|
|
- Optional agent tab: `/:companyPrefix/agents/:agentId?tab=git`
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- branch naming template
|
|
- optional remote provider token secret ref
|
|
- whether write actions are enabled or read-only
|
|
- whether the plugin always uses `project.primaryWorkspace` unless a different project workspace is chosen
|
|
- Git overview page:
|
|
- project picker and workspace picker
|
|
- current branch
|
|
- ahead/behind status
|
|
- dirty files summary
|
|
- recent commits
|
|
- active worktrees
|
|
- actions: refresh, create branch, create worktree, stage all, commit, open diff
|
|
- Project tab:
|
|
- opens in the project's primary workspace
|
|
- shows workspace metadata and repo binding (`cwd`, `repoUrl`, `repoRef`)
|
|
- shows branch, diff, and commit history for that project workspace
|
|
- Issue tab:
|
|
- resolves the issue's project and uses that project's workspace context
|
|
- "create branch from issue" action
|
|
- diff view scoped to the project's selected workspace
|
|
- link branch/worktree metadata to the issue
|
|
- Agent tab:
|
|
- shows the agent's branch, worktree, and dirty state
|
|
- shows recent commits produced by that agent
|
|
- if the agent is working inside a project workspace, links back to the project git tab
|
|
|
|
Core workflows:
|
|
|
|
- Board creates a branch from an issue and ties it to the project's primary workspace.
|
|
- Board opens a project page and reviews the diff for that project's workspace without leaving Paperclip.
|
|
- Board reviews the diff after a run without leaving Paperclip.
|
|
- Board opens a worktree list to understand parallel branches across agents.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.sidebar.register`
|
|
- `ui.page.register`
|
|
- `ui.detailTab.register` for `project`, `issue`, and `agent`
|
|
- `ui.action.register`
|
|
- `projects.read`
|
|
- `project.workspaces.read`
|
|
- optional `agent.tools.register` (e.g. `create-branch`, `get-diff`, `get-status`)
|
|
- optional `events.emit` (e.g. `plugin.@paperclip/plugin-git.push-detected`)
|
|
- `activity.log.write`
|
|
|
|
The plugin resolves workspace paths through `ctx.projects` and handles all git operations (status, diff, log, branch create, commit, worktree create, push) directly using git CLI or a git library.
|
|
|
|
Optional event subscriptions:
|
|
|
|
- `events.subscribe(issue.created)`
|
|
- `events.subscribe(issue.updated)`
|
|
- `events.subscribe(agent.run.finished)`
|
|
|
|
The git plugin can emit `plugin.@paperclip/plugin-git.push-detected` events that other plugins (e.g. GitHub Issues) subscribe to for cross-plugin coordination.
|
|
|
|
Note: GitHub/GitLab PR creation should likely live in a separate connector plugin rather than overloading the local git plugin.
|
|
|
|
## Linear Issue Tracking
|
|
|
|
Package idea: `@paperclip/plugin-linear`
|
|
|
|
This plugin syncs Paperclip work with Linear. It is useful for:
|
|
|
|
- importing backlog from Linear
|
|
- linking Paperclip issues to Linear issues
|
|
- syncing status, comments, and assignees
|
|
- mapping company goals/projects to external product planning
|
|
- giving board operators a single place to see sync health
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/linear`
|
|
- Main page: `/:companyPrefix/plugins/linear`
|
|
- Dashboard widget: `/:companyPrefix/dashboard`
|
|
- Optional issue tab: `/:companyPrefix/issues/:issueId?tab=linear`
|
|
- Optional project tab: `/:companyPrefix/projects/:projectId?tab=linear`
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- Linear API token secret ref
|
|
- workspace/team/project mappings
|
|
- status mapping between Paperclip and Linear
|
|
- sync direction: import only, export only, bidirectional
|
|
- comment sync toggle
|
|
- Linear overview page:
|
|
- sync health card
|
|
- recent sync jobs
|
|
- mapped projects and teams
|
|
- unresolved conflicts queue
|
|
- import actions for teams, projects, and issues
|
|
- Issue tab:
|
|
- linked Linear issue key and URL
|
|
- sync status and last synced time
|
|
- actions: link existing, create in Linear, resync now, unlink
|
|
- timeline of synced comments/status changes
|
|
- Dashboard widget:
|
|
- open sync errors
|
|
- imported vs linked issues count
|
|
- recent webhook/job failures
|
|
|
|
Core workflows:
|
|
|
|
- Board enables the plugin, maps a Linear team, and imports a backlog into Paperclip.
|
|
- Paperclip issue status changes push to Linear and Linear comments arrive back through webhooks.
|
|
- Board resolves mapping conflicts from the plugin page instead of silently drifting state.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.sidebar.register`
|
|
- `ui.page.register`
|
|
- `ui.dashboardWidget.register`
|
|
- `ui.detailTab.register` for `issue` and `project`
|
|
- `events.subscribe(issue.created)`
|
|
- `events.subscribe(issue.updated)`
|
|
- `events.subscribe(issue.comment.created)`
|
|
- `events.subscribe(project.updated)`
|
|
- `jobs.schedule`
|
|
- `webhooks.receive`
|
|
- `http.outbound`
|
|
- `secrets.read-ref`
|
|
- `plugin.state.read`
|
|
- `plugin.state.write`
|
|
- optional `issues.create`
|
|
- optional `issues.update`
|
|
- optional `issue.comments.create`
|
|
- optional `agent.tools.register` (e.g. `search-linear-issues`, `get-linear-issue`)
|
|
- `activity.log.write`
|
|
|
|
Important constraint:
|
|
|
|
- webhook processing should be idempotent and conflict-aware
|
|
- external IDs and sync cursors belong in plugin-owned state, not inline on core issue rows in the first version
|
|
|
|
## GitHub Issue Tracking
|
|
|
|
Package idea: `@paperclip/plugin-github-issues`
|
|
|
|
This plugin syncs Paperclip issues with GitHub Issues and optionally links PRs. It is useful for:
|
|
|
|
- importing repo backlogs
|
|
- mirroring issue status and comments
|
|
- linking PRs to Paperclip issues
|
|
- tracking cross-repo work from inside one company view
|
|
- bridging engineering workflow with Paperclip task governance
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/github-issues`
|
|
- Main page: `/:companyPrefix/plugins/github-issues`
|
|
- Dashboard widget: `/:companyPrefix/dashboard`
|
|
- Optional issue tab: `/:companyPrefix/issues/:issueId?tab=github`
|
|
- Optional project tab: `/:companyPrefix/projects/:projectId?tab=github`
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- GitHub App or PAT secret ref
|
|
- org/repo mappings
|
|
- label/status mapping
|
|
- whether PR linking is enabled
|
|
- whether new Paperclip issues should create GitHub issues automatically
|
|
- GitHub overview page:
|
|
- repo mapping list
|
|
- sync health and recent webhook events
|
|
- import backlog action
|
|
- queue of unlinked GitHub issues
|
|
- Issue tab:
|
|
- linked GitHub issue and optional linked PRs
|
|
- actions: create GitHub issue, link existing issue, unlink, resync
|
|
- comment/status sync timeline
|
|
- Dashboard widget:
|
|
- open PRs linked to active Paperclip issues
|
|
- webhook failures
|
|
- sync lag metrics
|
|
|
|
Core workflows:
|
|
|
|
- Board imports GitHub Issues for a repo into Paperclip.
|
|
- GitHub webhooks update status/comment state in Paperclip.
|
|
- A PR is linked back to the Paperclip issue so the board can follow delivery status.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.sidebar.register`
|
|
- `ui.page.register`
|
|
- `ui.dashboardWidget.register`
|
|
- `ui.detailTab.register` for `issue` and `project`
|
|
- `events.subscribe(issue.created)`
|
|
- `events.subscribe(issue.updated)`
|
|
- `events.subscribe(issue.comment.created)`
|
|
- `events.subscribe(plugin.@paperclip/plugin-git.push-detected)` (cross-plugin coordination)
|
|
- `jobs.schedule`
|
|
- `webhooks.receive`
|
|
- `http.outbound`
|
|
- `secrets.read-ref`
|
|
- `plugin.state.read`
|
|
- `plugin.state.write`
|
|
- optional `issues.create`
|
|
- optional `issues.update`
|
|
- optional `issue.comments.create`
|
|
- `activity.log.write`
|
|
|
|
Important constraint:
|
|
|
|
- keep "local git state" and "remote GitHub issue state" in separate plugins even if they work together — cross-plugin events handle coordination
|
|
|
|
## Grafana Metrics
|
|
|
|
Package idea: `@paperclip/plugin-grafana`
|
|
|
|
This plugin surfaces external metrics and dashboards inside Paperclip. It is useful for:
|
|
|
|
- company KPI visibility
|
|
- infrastructure/incident monitoring
|
|
- showing deploy, traffic, latency, or revenue charts next to work
|
|
- creating Paperclip issues from anomalous metrics
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/grafana`
|
|
- Main page: `/:companyPrefix/plugins/grafana`
|
|
- Dashboard widgets: `/:companyPrefix/dashboard`
|
|
- Optional goal tab: `/:companyPrefix/goals/:goalId?tab=metrics`
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- Grafana base URL
|
|
- service account token secret ref
|
|
- dashboard and panel mappings
|
|
- refresh interval
|
|
- optional alert threshold rules
|
|
- Dashboard widgets:
|
|
- one or more metric cards on the main dashboard
|
|
- quick trend view and last refresh time
|
|
- link out to Grafana and link in to the full Paperclip plugin page
|
|
- Full metrics page:
|
|
- selected dashboard panels embedded or proxied
|
|
- metric selector
|
|
- time range selector
|
|
- "create issue from anomaly" action
|
|
- Goal tab:
|
|
- metric cards relevant to a specific goal or project
|
|
|
|
Core workflows:
|
|
|
|
- Board sees service degradation or business KPI movement directly on the Paperclip dashboard.
|
|
- Board clicks into the full metrics page to inspect the relevant Grafana panels.
|
|
- Board creates a Paperclip issue from a threshold breach with a metric snapshot attached.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.dashboardWidget.register`
|
|
- `ui.page.register`
|
|
- `ui.detailTab.register` for `goal` or `project`
|
|
- `jobs.schedule`
|
|
- `http.outbound`
|
|
- `secrets.read-ref`
|
|
- `plugin.state.read`
|
|
- `plugin.state.write`
|
|
- optional `issues.create`
|
|
- optional `assets.write`
|
|
- `activity.log.write`
|
|
|
|
Optional event subscriptions:
|
|
|
|
- `events.subscribe(goal.created)`
|
|
- `events.subscribe(project.updated)`
|
|
|
|
Important constraint:
|
|
|
|
- start read-only first
|
|
- do not make Grafana alerting logic part of Paperclip core; keep it as additive signal and issue creation
|
|
|
|
## Child Process / Server Tracking
|
|
|
|
Package idea: `@paperclip/plugin-runtime-processes`
|
|
|
|
This plugin tracks long-lived local processes and dev servers started in project workspaces. It is useful for:
|
|
|
|
- seeing which agent started which local service
|
|
- tracking ports, health, and uptime
|
|
- restarting failed dev servers
|
|
- exposing process state alongside issue and run state
|
|
- making local development workflows visible to the board
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/runtime-processes`
|
|
- Main page: `/:companyPrefix/plugins/runtime-processes`
|
|
- Dashboard widget: `/:companyPrefix/dashboard`
|
|
- Process detail page: `/:companyPrefix/plugins/runtime-processes/:processId`
|
|
- Project tab: `/:companyPrefix/projects/:projectId?tab=processes`
|
|
- Optional agent tab: `/:companyPrefix/agents/:agentId?tab=processes`
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- whether manual process registration is allowed
|
|
- health check behavior
|
|
- whether operators can stop/restart processes
|
|
- log retention preferences
|
|
- Process list page:
|
|
- status table with name, command, cwd, owner agent, port, uptime, and health
|
|
- filters for running/exited/crashed processes
|
|
- actions: inspect, stop, restart, tail logs
|
|
- Project tab:
|
|
- filters the process list to the project's workspaces
|
|
- shows which workspace each process belongs to
|
|
- groups processes by project workspace
|
|
- Process detail page:
|
|
- process metadata
|
|
- live log tail
|
|
- health check history
|
|
- links to associated issue or run
|
|
- Agent tab:
|
|
- shows processes started by or assigned to that agent
|
|
|
|
Core workflows:
|
|
|
|
- An agent starts a dev server; the plugin detects and tracks it.
|
|
- Board opens a project and immediately sees the processes attached to that project's workspace.
|
|
- Board sees a crashed process on the dashboard and restarts it from the plugin page.
|
|
- Board attaches process logs to an issue when debugging a failure.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.sidebar.register`
|
|
- `ui.page.register`
|
|
- `ui.dashboardWidget.register`
|
|
- `ui.detailTab.register` for `project` and `agent`
|
|
- `projects.read`
|
|
- `project.workspaces.read`
|
|
- `plugin.state.read`
|
|
- `plugin.state.write`
|
|
- `activity.log.write`
|
|
|
|
The plugin resolves workspace paths through `ctx.projects` and handles process management (register, list, terminate, restart, read logs, health probes) directly using Node APIs.
|
|
|
|
Optional event subscriptions:
|
|
|
|
- `events.subscribe(agent.run.started)`
|
|
- `events.subscribe(agent.run.finished)`
|
|
|
|
## Stripe Revenue Tracking
|
|
|
|
Package idea: `@paperclip/plugin-stripe`
|
|
|
|
This plugin pulls Stripe revenue and subscription data into Paperclip. It is useful for:
|
|
|
|
- showing MRR and churn next to company goals
|
|
- tracking trials, conversions, and failed payments
|
|
- letting the board connect revenue movement to ongoing work
|
|
- enabling future financial dashboards beyond token costs
|
|
|
|
### UX
|
|
|
|
- Settings page: `/settings/plugins/stripe`
|
|
- Main page: `/:companyPrefix/plugins/stripe`
|
|
- Dashboard widgets: `/:companyPrefix/dashboard`
|
|
- Optional company/goal metric tabs if those surfaces exist later
|
|
|
|
Main screens and interactions:
|
|
|
|
- Plugin settings:
|
|
- Stripe secret key secret ref
|
|
- account selection if needed
|
|
- metric definitions such as MRR treatment and trial handling
|
|
- sync interval
|
|
- webhook signing secret ref
|
|
- Dashboard widgets:
|
|
- MRR card
|
|
- active subscriptions
|
|
- trial-to-paid conversion
|
|
- failed payment alerts
|
|
- Stripe overview page:
|
|
- time series charts
|
|
- recent customer/subscription events
|
|
- webhook health
|
|
- sync history
|
|
- action: create issue from billing anomaly
|
|
|
|
Core workflows:
|
|
|
|
- Board enables the plugin and connects a Stripe account.
|
|
- Webhooks and scheduled reconciliation keep plugin state current.
|
|
- Revenue widgets appear on the main dashboard and can be linked to company goals.
|
|
- Failed payment spikes or churn events can generate Paperclip issues for follow-up.
|
|
|
|
### Hooks needed
|
|
|
|
Recommended capabilities and extension points:
|
|
|
|
- `instance.settings.register`
|
|
- `ui.dashboardWidget.register`
|
|
- `ui.page.register`
|
|
- `jobs.schedule`
|
|
- `webhooks.receive`
|
|
- `http.outbound`
|
|
- `secrets.read-ref`
|
|
- `plugin.state.read`
|
|
- `plugin.state.write`
|
|
- `metrics.write`
|
|
- optional `issues.create`
|
|
- `activity.log.write`
|
|
|
|
Important constraint:
|
|
|
|
- Stripe data should stay additive to Paperclip core
|
|
- it should not leak into core budgeting logic, which is specifically about model/token spend in V1
|
|
|
|
## Specific Patterns From OpenCode Worth Adopting
|
|
|
|
## Adopt
|
|
|
|
- separate SDK package from runtime loader
|
|
- deterministic load order and precedence
|
|
- very small authoring API
|
|
- typed schemas for plugin inputs/config/tools
|
|
- tools as a first-class plugin extension point (namespaced, not override-by-collision)
|
|
- internal extensions using the same registration shapes as external ones when reasonable
|
|
- plugin load errors isolated from host startup when possible
|
|
- explicit community-facing plugin docs and example templates
|
|
- test harness and starter template for low authoring friction
|
|
- hot plugin lifecycle without server restart (enabled by out-of-process workers)
|
|
- formal SDK versioning with multi-version host support
|
|
|
|
## Adapt, not copy
|
|
|
|
- local path loading
|
|
- dependency auto-install
|
|
- hook mutation model
|
|
- built-in override behavior
|
|
- broad runtime context objects
|
|
|
|
## Avoid
|
|
|
|
- project-local arbitrary code loading
|
|
- implicit trust of npm packages at startup
|
|
- plugins overriding core invariants
|
|
- unsandboxed in-process execution as the default extension model
|
|
|
|
## Suggested Rollout Plan
|
|
|
|
## Phase 0: Harden the seams that already exist
|
|
|
|
- formalize adapter/storage/secret/run-log registries as "platform modules"
|
|
- remove ad-hoc fallback behavior where possible
|
|
- document stable registration contracts
|
|
|
|
## Phase 1: Add connector plugins first
|
|
|
|
This is the highest-value, lowest-risk plugin category.
|
|
|
|
Build:
|
|
|
|
- plugin manifest
|
|
- global install/update lifecycle
|
|
- global plugin config and optional company-mapping storage
|
|
- secret ref access
|
|
- typed domain event subscription
|
|
- scheduled jobs
|
|
- webhook endpoints
|
|
- activity logging helpers
|
|
- plugin UI bundle loading, host bridge, `@paperclipai/plugin-sdk/ui`
|
|
- extension slot mounting for pages, tabs, widgets, sidebar entries
|
|
- auto-generated settings form from `instanceConfigSchema`
|
|
- bridge error propagation (`PluginBridgeError`)
|
|
- plugin-contributed agent tools
|
|
- plugin-to-plugin events (`plugin.<pluginId>.*` namespace)
|
|
- event filtering (server-side, per-subscription)
|
|
- graceful shutdown with configurable deadlines
|
|
- plugin logging and health dashboard
|
|
- uninstall with data retention grace period
|
|
- `@paperclipai/plugin-test-harness` and `create-paperclip-plugin` starter template
|
|
- hot plugin lifecycle (install, uninstall, upgrade, config change without server restart)
|
|
- SDK versioning with multi-version host support and deprecation policy
|
|
|
|
This phase would immediately cover:
|
|
|
|
- Linear
|
|
- GitHub
|
|
- Grafana
|
|
- Stripe
|
|
- file browser
|
|
- terminal
|
|
- git workflow
|
|
- child process/server tracking
|
|
|
|
Workspace plugins do not require additional host APIs — they resolve workspace paths through `ctx.projects` and handle filesystem, git, PTY, and process operations directly.
|
|
|
|
## Phase 2: Consider richer UI and plugin packaging
|
|
|
|
Only after Phase 1 is stable:
|
|
|
|
- iframe-based isolation for untrusted third-party plugin UI bundles
|
|
- signed/verified plugin packages
|
|
- plugin marketplace
|
|
- optional custom plugin storage backends or migrations
|
|
|
|
## Recommended Architecture Decision
|
|
|
|
If I had to collapse this report into one architectural decision, it would be:
|
|
|
|
Paperclip should not implement "an OpenCode-style generic in-process hook system."
|
|
Paperclip should implement "a plugin platform with multiple trust tiers":
|
|
|
|
- trusted platform modules for low-level runtime integration
|
|
- typed out-of-process plugins for instance-wide integrations and automation
|
|
- plugin-contributed agent tools (namespaced, capability-gated)
|
|
- plugin-shipped UI bundles rendered in host extension slots via a typed bridge with structured error propagation
|
|
- plugin-to-plugin events for cross-plugin coordination
|
|
- auto-generated settings UI from config schema
|
|
- core-owned invariants that plugins can observe and act around, but not replace
|
|
- plugin observability, graceful lifecycle management, and a test harness for low authoring friction
|
|
- hot plugin lifecycle — no server restart for install, uninstall, upgrade, or config changes
|
|
- SDK versioning with multi-version host support and clear deprecation policy
|
|
|
|
That gets the upside of `opencode`'s extensibility without importing the wrong threat model.
|
|
|
|
## Concrete Next Steps I Would Take In Paperclip
|
|
|
|
1. Write a short extension architecture RFC that formalizes the distinction between `platform modules` and `plugins`.
|
|
2. Introduce a small plugin manifest type in `packages/shared` and a `plugins` install/config section in the instance config.
|
|
3. Build a typed domain event bus around existing activity/live-event patterns, with server-side event filtering and a `plugin.*` namespace for cross-plugin events. Keep core invariants non-hookable.
|
|
4. Implement plugin MVP: global install/config, secret refs, jobs, webhooks, plugin UI bundles, extension slots, auto-generated settings forms, bridge error propagation.
|
|
5. Add agent tool contributions — plugins register namespaced tools that agents can call during runs.
|
|
6. Add plugin observability: structured logging via `ctx.logger`, health dashboard, internal health events.
|
|
7. Add graceful shutdown policy and uninstall data lifecycle with retention grace period.
|
|
8. Ship `@paperclipai/plugin-test-harness` and `create-paperclip-plugin` starter template.
|
|
9. Implement hot plugin lifecycle — install, uninstall, upgrade, and config changes without server restart.
|
|
10. Define SDK versioning policy — semver, multi-version host support, deprecation timeline, migration guides, published compatibility matrix.
|
|
11. Build workspace plugins (file browser, terminal, git, process tracking) that resolve workspace paths from the host and handle OS-level operations directly.
|