mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-07 07:23:08 +02:00
## 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>
112 lines
6.1 KiB
Markdown
112 lines
6.1 KiB
Markdown
# 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.
|
|
|
|
```json
|
|
{
|
|
"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.
|