Files
PaperClipAI/doc/plugins/DISTRIBUTION-PLUGINS.md
T
Devin FoleyandPaperclip 4ad5977d24 fix(plugins): retain source and privacy boundaries across restarts
Keep pending rollback metadata current without implicitly approving capabilities or enabling a previously disabled plugin. Restrict offline caches to public hashed assets and reject npm fallback for persisted image installs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-19 08:57:51 -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.