Install plugins from the interface: upload a package, pick from the official catalogue, later add custom catalogues #38

Closed
opened 2026-09-05 14:06:43 +00:00 by tiagoagueda · 0 comments
Owner

Observation

plugins should be installed either by uploading zip, from an official manifest, in the future maybe a custom manifest

What exists today

  • Installing a plugin is a shell command. docs/PLUGINS.md (lines 87–94): uv pip install my-postulo-myboard into the same environment, restart, and the source appears; uninstalling the package removes it. The registry (plugins/registry.py) loads entry points at startup, and a broken plugin disables itself and is logged rather than taking capture down.
  • The container has nowhere for a plugin to live. The image installs a locked environment at build time (uv sync --locked), runs as the non-root postulo user, and the only writable place is the /app/data volume. A pip install inside the running container lands in the image layer and is gone at the next upgrade. #5 asked this question ("how plugins reach a container") and left it open; this issue answers it.
  • Server settings (#24) already reserves a Plugins section listing what is installed, with versions and whether each loaded cleanly. This is where installing belongs.

Shape

1. A plugins directory on the data volume. POSTULO_PLUGINS_DIR, default /app/data/plugins in the image and data/plugins otherwise, added to sys.path at startup (site.addsitedir, so .pth files work), with a plugins.json beside it recording what is installed and where it came from: name, version, source (upload, catalogue name, URL), SHA-256, when, by whom. Because the record lives on the volume, plugins survive an image upgrade: the entrypoint runs manage.py plugins sync at boot, which reinstalls anything the record lists that the directory lacks — a new Python minor in the image, say.

2. Three sources, in the order they arrive.

  • Upload a package. A wheel — which is a zip, and what "uploading a zip" means for Python — or an sdist archive. The administrator uploads it; Postulo reads the metadata before installing and shows it for confirmation: name, version, licence, maintainer, which postulo.* entry points it declares, the Postulo version range it says it supports, its dependencies. Refused outright: anything that is not pure Python (py3-none-any; the image has no compiler and should not), anything with no Postulo entry point, anything whose dependencies would change the version of a package Postulo itself pins — the install runs with the core's lock as a constraint, so a plugin can never downgrade or upgrade the core's own dependencies, and the refusal names the package.
  • The official catalogue. A JSON index published by the project — a postulo-plugins repository on Forgejo, its raw URL the default of POSTULO_PLUGIN_CATALOGUES — listing, per plugin: name, description, maintainer, licence, repository, and per version a wheel URL, SHA-256, compatible Postulo range and the entry-point kinds it provides. The Plugins page shows it with Install, Update when the index has a newer compatible version, Remove. The index is signed (minisign or plain Ed25519, the project's public key shipped in the release), and every wheel is checked against the SHA-256 the signed index carries — so a mirror or a hijacked download host cannot ship code. Fetched only when the administrator opens the page or presses Check for updates, never in the background by default: Postulo makes no request on its own. Being in the catalogue means the project has looked at the plugin — contract, licence, that it does nothing with the network or secrets beyond what it says — and that is stated as a review, not a guarantee.
  • Custom catalogues, later. The administrator adds an index URL and its public key; same format, same signature check, labelled third-party catalogue everywhere its plugins appear. This is the modularity statement applied to distribution: the project curates one catalogue and prevents nobody from running another.

3. Activation. Entry points are read from sys.path, and the registry already supports a refresh (available_sources(refresh=True)), so a plugin that only registers entry points — a source, a notifier, a store — can be activated without a restart once its directory is on the path. A plugin that adds a Django app (models, URLs, templates) needs one; the page says so and offers Restart now, which asks gunicorn to exit gracefully and lets Compose bring it back. The record on the volume makes that safe.

4. The security posture, said plainly on the page. Installing a plugin is running someone's code inside Postulo, with access to everything on the instance. Only administrators can do it; the confirmation screen shows what is about to be installed; the catalogue is signed; a per-plugin Disable switch stops it without removing it; a failing plugin is isolated exactly as today. No automatic updates. An update is a code change and an administrator clicks it; a notification that updates exist can go through #4 for those who want it.

5. The same code from the command line. manage.py plugins list | install <wheel or name> | update | remove | sync, which is what the entrypoint calls and what a GitOps-minded operator scripts. POSTULO_PLUGINS="postulo-apprise postulo-paperless" in the environment installs from the catalogue at first boot. The FROM postulo Dockerfile approach from #5 stays documented for anyone who wants an immutable image with plugins baked in.

6. Documentation. docs/PLUGINS.md gains Publishing to the catalogue — a pull request to postulo-plugins with the metadata and the wheel URL from the author's own release, plus what the review looks at. Installing Postulo gains a Plugins section. #5, #15, #16 and #34 are the first catalogue entries; #17, #18 and #19 run outside Postulo and are listed as companion tools with links, not installable packages.

Classification

Enhancement. Not breaking: uv pip install keeps working exactly as documented; the directory and the record are new; nothing changes for an instance with no plugins.

Depends on

  • #24 — the Server settings area, whose Plugins section this fills
  • #11 — most catalogue plugins need a connection; not blocking

Open questions

  1. Signature scheme: minisign (one tool, one key file) or Sigstore-style keyless signing? Proposal: minisign; a self-hosted project should not depend on a hosted transparency log to install a plugin.
  2. Should the catalogue index also carry a compatibility tested with field per Postulo release, filled by CI of the plugin repositories, so Install can warn "not yet tested with 0.3"? Cheap if the plugin CIs publish it; decide when the second plugin exists.
  3. Where does the Restart now button draw the line: allowed on Compose (where the process is supervised) and hidden elsewhere?
## Observation > plugins should be installed either by uploading zip, from an official manifest, in the future maybe a custom manifest ## What exists today - **Installing a plugin is a shell command.** `docs/PLUGINS.md` (lines 87–94): `uv pip install my-postulo-myboard` into the same environment, restart, and the source appears; uninstalling the package removes it. The registry (`plugins/registry.py`) loads entry points at startup, and a broken plugin disables itself and is logged rather than taking capture down. - **The container has nowhere for a plugin to live.** The image installs a locked environment at build time (`uv sync --locked`), runs as the non-root `postulo` user, and the only writable place is the `/app/data` volume. A `pip install` inside the running container lands in the image layer and is gone at the next upgrade. #5 asked this question ("how plugins reach a container") and left it open; this issue answers it. - **Server settings** (#24) already reserves a *Plugins* section listing what is installed, with versions and whether each loaded cleanly. This is where installing belongs. ## Shape **1. A plugins directory on the data volume.** `POSTULO_PLUGINS_DIR`, default `/app/data/plugins` in the image and `data/plugins` otherwise, added to `sys.path` at startup (`site.addsitedir`, so `.pth` files work), with a **`plugins.json`** beside it recording what is installed and where it came from: name, version, source (upload, catalogue name, URL), SHA-256, when, by whom. Because the record lives on the volume, **plugins survive an image upgrade**: the entrypoint runs `manage.py plugins sync` at boot, which reinstalls anything the record lists that the directory lacks — a new Python minor in the image, say. **2. Three sources, in the order they arrive.** - **Upload a package.** A wheel — which is a zip, and what "uploading a zip" means for Python — or an sdist archive. The administrator uploads it; Postulo reads the metadata *before* installing and shows it for confirmation: name, version, licence, maintainer, which `postulo.*` entry points it declares, the Postulo version range it says it supports, its dependencies. Refused outright: anything that is not pure Python (`py3-none-any`; the image has no compiler and should not), anything with no Postulo entry point, anything whose dependencies would change the version of a package Postulo itself pins — the install runs with the core's lock as a **constraint**, so a plugin can never downgrade or upgrade the core's own dependencies, and the refusal names the package. - **The official catalogue.** A JSON index published by the project — a `postulo-plugins` repository on Forgejo, its raw URL the default of `POSTULO_PLUGIN_CATALOGUES` — listing, per plugin: name, description, maintainer, licence, repository, and per version a wheel URL, SHA-256, compatible Postulo range and the entry-point kinds it provides. The Plugins page shows it with *Install*, *Update* when the index has a newer compatible version, *Remove*. The index is **signed** (minisign or plain Ed25519, the project's public key shipped in the release), and every wheel is checked against the SHA-256 the signed index carries — so a mirror or a hijacked download host cannot ship code. **Fetched only when the administrator opens the page or presses *Check for updates***, never in the background by default: Postulo makes no request on its own. Being in the catalogue means the project has looked at the plugin — contract, licence, that it does nothing with the network or secrets beyond what it says — and that is stated as a review, not a guarantee. - **Custom catalogues**, later. The administrator adds an index URL and its public key; same format, same signature check, labelled *third-party catalogue* everywhere its plugins appear. This is the modularity statement applied to distribution: the project curates one catalogue and prevents nobody from running another. **3. Activation.** Entry points are read from `sys.path`, and the registry already supports a refresh (`available_sources(refresh=True)`), so a plugin that only registers entry points — a source, a notifier, a store — can be activated **without a restart** once its directory is on the path. A plugin that adds a Django app (models, URLs, templates) needs one; the page says so and offers *Restart now*, which asks gunicorn to exit gracefully and lets Compose bring it back. The record on the volume makes that safe. **4. The security posture, said plainly on the page.** Installing a plugin is running someone's code inside Postulo, with access to everything on the instance. Only administrators can do it; the confirmation screen shows what is about to be installed; the catalogue is signed; a per-plugin **Disable** switch stops it without removing it; a failing plugin is isolated exactly as today. **No automatic updates.** An update is a code change and an administrator clicks it; a notification that updates exist can go through #4 for those who want it. **5. The same code from the command line.** `manage.py plugins list | install <wheel or name> | update | remove | sync`, which is what the entrypoint calls and what a GitOps-minded operator scripts. `POSTULO_PLUGINS="postulo-apprise postulo-paperless"` in the environment installs from the catalogue at first boot. The `FROM postulo` Dockerfile approach from #5 stays documented for anyone who wants an immutable image with plugins baked in. **6. Documentation.** `docs/PLUGINS.md` gains *Publishing to the catalogue* — a pull request to `postulo-plugins` with the metadata and the wheel URL from the author's own release, plus what the review looks at. *Installing Postulo* gains a Plugins section. #5, #15, #16 and #34 are the first catalogue entries; #17, #18 and #19 run outside Postulo and are listed as *companion tools* with links, not installable packages. ## Classification Enhancement. Not breaking: `uv pip install` keeps working exactly as documented; the directory and the record are new; nothing changes for an instance with no plugins. ## Depends on - #24 — the Server settings area, whose Plugins section this fills - #11 — most catalogue plugins need a connection; not blocking ## Open questions 1. Signature scheme: minisign (one tool, one key file) or Sigstore-style keyless signing? Proposal: minisign; a self-hosted project should not depend on a hosted transparency log to install a plugin. 2. Should the catalogue index also carry a *compatibility tested with* field per Postulo release, filled by CI of the plugin repositories, so *Install* can warn "not yet tested with 0.3"? Cheap if the plugin CIs publish it; decide when the second plugin exists. 3. Where does the *Restart now* button draw the line: allowed on Compose (where the process is supervised) and hidden elsewhere?
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 14:06:43 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#38
No description provided.