Files
PaperClipAI/doc/plugins/DISTRIBUTION-PLUGINS.md
T
Devin FoleyandPaperclip b70641f23f feat(plugins): support image catalogs and persistent application overlays (#13646)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Plugins extend the application without adding each integration to
Core.
> - A downstream image needs a way to supply prebuilt plugins.
> - Some plugin UI must stay mounted as users move between pages.
> - This change adds an image catalog and a persistent application slot.
> - Operators can upgrade or remove these plugins through their image
and configuration.

## Linked Issues or Issue Description

**Subsystem affected**

Plugin packaging, activation and application UI.

**Problem or motivation**

The built-in plugin catalog is fixed in Core source. Downstream images
cannot add entries through an explicit catalog. Existing page slots also
cannot preserve a small application overlay across route changes.

**Proposed solution**

Read a bounded catalog of prebuilt plugins from the image. Verify its
files before importing manifests. Use the existing managed selection and
plugin lifecycle. Add an `appShellOverlay` slot with account and company
cleanup.

**Alternatives considered**

A downstream fork adds merge work. Script injection provides no plugin
lifecycle. A separate runtime download system adds a second distribution
channel.

**Roadmap alignment**

This extends the existing plugin system. Related PR #9006 covers runtime
install replication; this change covers immutable image contents. PR
#12555 covers CLI scaffolding. Neither provides this catalog or
application slot. The maintainer requested this work directly.

## What Changed

- Validate catalog identities, confined paths, package versions and
bundle hashes before importing code.
- Apply image selection to persisted plugin installs, including removal
and rollback. Adopt the verified image path from existing npm/local
installs and bind runtime worker/UI entrypoints to verified package
declarations.
- Mount application overlays in both UI shells. Preserve route state and
clear it on account, company and onboarding changes.
- Restrict service-worker offline storage/fallback to hashed public
assets in a separate cache namespace; exclude application HTML and
extension/API data, including after worker restart.
- Document the packaging contract, trust model and rollback
requirements.

## Verification

- Passed `pnpm -r typecheck`, `pnpm build`, and `pnpm
check:token-gates`. Affected server/UI typechecks and builds, plus token
gates, passed again after rebasing onto current master; the 124 focused
tests also passed after rebase.
- Latest focused verification: 124 tests in nine files passed for
catalog/reconciliation/loader, overlay lifecycle, Layout and
service-worker policy. The broader UI/shared/SDK run passed 7,204 tests
in 690 files with canonical `TMPDIR`.
- Real disposable Core/PostgreSQL: catalog install, selection removal,
0.1.0→0.1.1→0.1.0, same-version npm/legacy-path adoption, and
preservation of disabled status passed. Added permissions entered
`upgrade_pending`, withheld UI across restart, and activated only after
explicit operator enable.
- Real Chromium: desktop/mobile layout, route draft retention and
Escape/focus passed with mocked extension responses. A persistent
browser restart retained public hashed-asset offline fallback while
refusing seeded legacy/current private entries and legacy HTML.
- Full `pnpm test:run`: 12,539 passed; 17 failed across six existing
files, stopping later phases. macOS read-only directory renames fail in
runtime-skill-cache and company-skills-service; email tests require an
absent local AgentMail fixture. Native runner/comment-redaction passed
in isolation after temporary Rust setup; agent-conversations also passed
in isolation. No unrelated source was changed to hide failures.
- After rebase, two unchanged chat timing tests failed in CI and passed
locally in isolation. Their CI shard passed on its single retry. All
other current-head CI jobs passed on the initial run; review is 5/5 with
no unresolved threads.
- No live deployment or external plugin service was used.

## Risks

- Plugins are trusted code. The catalog detects packaging errors; it
does not authenticate an untrusted image builder.
- Invalid catalogs fail startup. Images must contain the catalog and
bundles together, with stable directories.
- A host older than this contract lacks the activation guard. Disable
added plugins and remove their configuration keys before reverting to
it.
- Offline navigation now returns 503 instead of replaying cached
application HTML. Only public build assets have offline fallback.
- Rolling back an unapproved permission change retains the approval
gate; review the current manifest and explicitly enable it. A reduced
permission set cannot establish prior approval or prior enabled status.
- Plugin data migrations need their own rollback policy. This change
retains installed records and does not reverse migrations.

## Model Used

- OpenAI GPT-6 (Codex), model ID `gpt-6`, with repository inspection,
code execution and browser verification. The runtime does not expose an
exact context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (relevant suites; broad
macOS server-run exceptions are documented above)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green (fresh run on 488b3754ae; chat
shard passed its single retry)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(fresh review on 488b3754ae)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-19 09:18:10 -07:00

6.1 KiB

Plugins supplied by an application distribution

A downstream image can add prebuilt plugins without changing Paperclip's built-in catalog. The operator owns the image and trusts its plugin code. This is packaging and activation policy, not a sandbox or entitlement system. Ordinary self-hosted images need no catalog and keep their existing behavior.

Image layout

Use distribution/catalog.json beneath PAPERCLIP_BUNDLED_PLUGIN_ROOT (default /app/packages/plugins). Each plugin has a stable directory below distribution/, containing its package.json, compiled manifest, worker and optional UI. Bundle runtime dependencies; startup never installs them.

{
  "schemaVersion": 1,
  "plugins": [{
    "key": "example-extension",
    "pluginKey": "example.extension",
    "version": "1.0.0",
    "directory": "example-extension",
    "digest": "sha256:<64 lowercase hex characters>"
  }]
}

The digest covers a sorted depth-first file inventory. Each entry is [relativePosixPath, "sha256:" + sha256(fileBytes), permissionBits & 0777]. Hash the UTF-8 JSON serialization of the inventory and prefix it with sha256:. The server's distributionBundleDigest implements this contract. There are no symbolic links or special files. A bundle is limited to 10,000 files, 256 MiB and 32 directory levels. Catalog keys, plugin IDs and directory names must be unique; a distribution cannot replace a built-in key or ID.

On startup, the host validates the catalog, hashes the bundle before importing its executable manifest, and validates package version and confined prebuilt entrypoints. Malformed catalogs and integrity failures stop startup. Deploy the catalog and bundles atomically as part of the image; keep them read-only in operation. This detects packaging errors but does not authenticate an untrusted image builder. Image provenance and signatures remain deployment responsibilities.

Selection, upgrades and rollback

Managed instances select a distribution key through the existing plugins.autoInstall list. Existing install, capability validation, API compatibility, worker and health mechanisms apply. A worker or install failure is recorded as a plugin error without taking down the application.

The catalog alone does not auto-enable plugins on self-hosted instances. Operators can explicitly install catalog entries through the normal plugin CLI. The package's manifest ID and version must match the catalog. The manifest's worker and optional UI entrypoints must match the verified package.json declarations and stay inside the bundle.

At boot, selected distribution entries adopt the current image's package path even when a previous npm or local install has the same version. Reconciliation keeps the registry ID, configuration and stored state. With unchanged permissions, operator-disabled status is retained. A replacement that adds capabilities is saved atomically in upgrade_pending, even for same-version bundles. It cannot activate until an operator reviews the manifest and enables it through the normal plugin lifecycle. Invalid capability declarations are rejected before persistence. Runtime refreshes also reject unapproved capability additions before starting code. Rolling back an unapproved replacement refreshes the displayed manifest but retains upgrade_pending. Review the rollback manifest and explicitly enable it to resume. A smaller capability set alone cannot prove prior approval: it may retain an unapproved permission, and the plugin may originally have been disabled. Ordinary upgrades/downgrades of an approved, ready plugin continue automatically.

Keep each key's directory stable across releases. The activation guard also covers persisted installs: a plugin removed from the image catalog, or no longer selected in managed configuration, cannot activate on restart. Its stored image path remains the source marker if the directory disappears; package resolution cannot substitute an npm copy. Keep the catalog root stable as well. An explicit operator reinstall changes a package's source; editing database rows or replacing the catalog root is outside this image-selection contract. Plugin database records remain for rollback. Plugin data migrations must themselves support the intended rollback window; removing a bundle does not undo them.

A deployment controller must generate plugins.autoInstall from the target image's catalog. A union of catalogs from different releases is insufficient: an older image rejects a key it does not know. Before reverting to a host version that predates this catalog contract, disable the distribution plugins and remove their keys from configuration. Such older hosts do not have the new activation guard.

Persistent application UI

The appShellOverlay slot requires ui.action.register. It receives the usual PluginWidgetProps context. It mounts once in both application shells and survives route navigation. It is disposed when the account or selected company changes, during onboarding, and on sign-out. It is not mounted on login pages. Local-trusted mode has no login requirement: its sessionless board may mount overlays, but transitions to or from an account still dispose the prior state.

The host positions contributions above the mobile navigation and stacks them at the bottom right. Each plugin owns its launcher, panel, keyboard handling, focus restoration, accessible labels and request cancellation. Use a bounded, responsive panel. This slot is not a launcher placement zone and does not replace modal/launcher APIs. Errors remain inside the existing plugin mount error boundary.

UI code is trusted browser code. Host context is display context, never proof of server authorization. A distribution backend must independently validate the signed-in session and enforce company, tenant and user access rules for every read and mutation. Keep provider secrets out of plugin UI and manifests. The service worker's offline cache accepts only same-origin, hashed build assets under /assets/. It does not store or replay application HTML, extension routes or API data. This policy remains in effect after worker restarts and does not read the arbitrary-response caches created by older workers.