An official plugin's version should say which Postulo it is for, and requires_postulo is never checked #186

Closed
opened 2026-09-12 09:33:13 +00:00 by tiagoagueda · 2 comments
Owner

The rule

An official plugin's version tracks the core's. postulo-mcp 0.3.0 is the one that works
with Postulo 0.3.0; so is postulo-paperless 0.3.0, and every other plugin the project
publishes. Somebody who knows which Postulo they run knows which plugin to install, without
reading a compatibility table.

This is not a new idea here — it is already the rule for the built-ins, written down in
plugins/base.py with its reasoning:

The version is Postulo's own, because that is the truth: these ship with the application
and change when it does. A built-in with a literal version number identifies nothing —
version = "1.0" meant 1.0 the day it was written and would have gone on meaning it.

test_plugin_manifests.py enforces it for built-ins today: a manifest whose version is not
postulo.__version__ fails. What this issue does is extend the same reasoning outward, to
the plugins that ship in their own repositories.

Where everything actually is

Repository Version Core
postulo 0.2.1 —
postulo-mcp 0.1.0
postulo-paperless 0.1.0
postulo-imap 0.1.0
postulo-apprise 0.1.0
postulo-dav 0.1.0
postulo-chromium 0.1.0 package.json, not pyproject.toml
postulo-firefox 0.1.0 package.json
postulo-helloworld 0.1.0
postulo-templates none at all not a package

Every one is 0.1.0 and none has ever been released, so nothing has to be migrated — the rule
can simply start.

The field exists and nothing enforces it

catalogue.py already carries per release:

requires_postulo: str = ""

and the module docstring already promises it:

A catalogue is one JSON file … listing plugins and — per version — a wheel to fetch, its
SHA-256, the Postulo versions it is compatible with, and what it provides.

It is parsed at catalogue.py:200, stored on Release, set by a fixture in
test_plugin_install.py:341 — and never compared to anything. The docstring lists the
checks that are fatal:

  1. the index's signature verifies against the configured key (Ed25519);
  2. the plugin and version asked for are in the index;
  3. the wheel that arrives matches the SHA-256 the signed index gave.

Compatibility is not among them. A release declaring requires_postulo: ">=0.5" installs
into 0.3.0 today, and the first anyone knows is an ImportError in a log.

The version scheme, and the cost of getting it wrong

Strict full lockstep is a trap. If postulo-mcp must be exactly 0.3.0 to pair with core
0.3.0, then every core release forces a release of all eight plugins whose code did not
change, and a plugin bugfix cannot ship at all without a version claiming a core release
that does not exist.

Lockstep on major.minor, free patch gives the rule its whole benefit and none of that:

postulo 0.3.x  <->  postulo-mcp 0.3.*     any patch of one works with any patch of the other
postulo-mcp 0.3.4                          the fifth fix to the 0.3 plugin, no core release implied

So requires_postulo for an official plugin is ~=0.3.0 — the minor is the contract, the
patch is the plugin's own. That reads the same way to a person ("0.3 plugin, 0.3 Postulo")
and lets each repository fix its own bugs.

What to do

  • Decide and write down the scheme — major.minor lockstep, patch free — in
    CONTRIBUTING.md and in the wiki's Writing a plugin, which today shows version = "1.0"
    in its examples and says nothing about pairing.
  • Check requires_postulo at install, and make it a fourth fatal check beside the other
    three, with a refusal that names both versions. A plugin that declares nothing keeps
    installing — the field is optional and a third-party plugin may reasonably not know.
  • Say it on the plugins page. An installed plugin that no longer matches the running core
    should be visible there, not only at import time. This meets #184, which is already
    redrawing those tags.
  • Bump each official repository to the core's minor at its first release. Nothing has
    been published, so this costs one line per repository and no migration.
  • The two extensions are package.json, not pyproject.toml, and they are not installed
    through the catalogue at all — they go through a browser store. The rule still applies to
    the number a person sees; the enforcement cannot.
  • postulo-templates has no version. Decide whether a template pack is versioned like the
    rest or deliberately not; either answer is fine, silence is not.
  • postulo/postulo-mcp#1 plans that repository's first release and says to tag v0.1.0. Under
    this rule it is v0.3.0. Updated there.
  • #184 is redrawing the plugin tags and is where a "needs a newer Postulo" state would show.
## The rule **An official plugin's version tracks the core's.** postulo-mcp 0.3.0 is the one that works with Postulo 0.3.0; so is postulo-paperless 0.3.0, and every other plugin the project publishes. Somebody who knows which Postulo they run knows which plugin to install, without reading a compatibility table. This is not a new idea here — it is already the rule for the built-ins, written down in `plugins/base.py` with its reasoning: > The version is Postulo's own, because that is the truth: these ship with the application > and change when it does. **A built-in with a literal version number identifies nothing** — > `version = "1.0"` meant 1.0 the day it was written and would have gone on meaning it. `test_plugin_manifests.py` enforces it for built-ins today: a manifest whose version is not `postulo.__version__` fails. What this issue does is extend the same reasoning outward, to the plugins that ship in their own repositories. ## Where everything actually is | Repository | Version | Core | | --- | --- | --- | | postulo | **0.2.1** | — | | postulo-mcp | 0.1.0 | | | postulo-paperless | 0.1.0 | | | postulo-imap | 0.1.0 | | | postulo-apprise | 0.1.0 | | | postulo-dav | 0.1.0 | | | postulo-chromium | 0.1.0 | `package.json`, not `pyproject.toml` | | postulo-firefox | 0.1.0 | `package.json` | | postulo-helloworld | 0.1.0 | | | postulo-templates | **none at all** | not a package | Every one is 0.1.0 and none has ever been released, so nothing has to be migrated — the rule can simply start. ## The field exists and nothing enforces it `catalogue.py` already carries per release: requires_postulo: str = "" and the module docstring already promises it: > A catalogue is one JSON file … listing plugins and — per version — a wheel to fetch, its > SHA-256, **the Postulo versions it is compatible with**, and what it provides. It is parsed at `catalogue.py:200`, stored on `Release`, set by a fixture in `test_plugin_install.py:341` — and **never compared to anything**. The docstring lists the checks that are fatal: > 1. the index's signature verifies against the configured key (Ed25519); > 2. the plugin and version asked for are in the index; > 3. the wheel that arrives matches the SHA-256 the signed index gave. Compatibility is not among them. A release declaring `requires_postulo: ">=0.5"` installs into 0.3.0 today, and the first anyone knows is an ImportError in a log. ## The version scheme, and the cost of getting it wrong **Strict full lockstep is a trap.** If postulo-mcp must be exactly 0.3.0 to pair with core 0.3.0, then every core release forces a release of all eight plugins whose code did not change, and a plugin bugfix cannot ship at all without a version claiming a core release that does not exist. **Lockstep on major.minor, free patch** gives the rule its whole benefit and none of that: postulo 0.3.x <-> postulo-mcp 0.3.* any patch of one works with any patch of the other postulo-mcp 0.3.4 the fifth fix to the 0.3 plugin, no core release implied So `requires_postulo` for an official plugin is `~=0.3.0` — the minor is the contract, the patch is the plugin's own. That reads the same way to a person ("0.3 plugin, 0.3 Postulo") and lets each repository fix its own bugs. ## What to do - **Decide and write down the scheme** — major.minor lockstep, patch free — in `CONTRIBUTING.md` and in the wiki's *Writing a plugin*, which today shows `version = "1.0"` in its examples and says nothing about pairing. - **Check `requires_postulo` at install**, and make it a fourth fatal check beside the other three, with a refusal that names both versions. A plugin that declares nothing keeps installing — the field is optional and a third-party plugin may reasonably not know. - **Say it on the plugins page.** An installed plugin that no longer matches the running core should be visible there, not only at import time. This meets #184, which is already redrawing those tags. - **Bump each official repository to the core's minor at its first release.** Nothing has been published, so this costs one line per repository and no migration. - **The two extensions are `package.json`**, not `pyproject.toml`, and they are not installed through the catalogue at all — they go through a browser store. The rule still applies to the number a person sees; the enforcement cannot. - **postulo-templates has no version.** Decide whether a template pack is versioned like the rest or deliberately not; either answer is fine, silence is not. ## Related - postulo/postulo-mcp#1 plans that repository's first release and says to tag `v0.1.0`. Under this rule it is `v0.3.0`. Updated there. - #184 is redrawing the plugin tags and is where a "needs a newer Postulo" state would show.
Author
Owner

Decided: the lock is on the major

Not ~=0.3.0 as the issue proposed. What is enforced is the major:

requires_postulo = ">=0.3,<1.0"

A plugin declares the floor it needs and keeps working across core's minors until the next
major. The number it carries still tracks the core it was released beside — postulo-mcp
0.3.0 ships with Postulo 0.3.0 — so a person can still pair them at a glance; what changed is
that the installer no longer refuses a 0.3 plugin on core 0.4.

One consequence to be aware of, because 0.x is not 1.x

Semantic versioning puts every 0.x in the same major, so a major-only lock during 0.x is a
floor and a ceiling at 1.0 and nothing in between. That is genuinely loose while Postulo is
pre-1.0, because 0.x is exactly where breaking changes land in the minor — a plugin
written against 0.3's plugin API will install happily on 0.6.

That is a reasonable trade while nothing has shipped and both sides move together, and it
becomes the right rule the moment Postulo is 1.x. It is worth knowing rather than
discovering: a plugin that a core release actually breaks has to raise its own floor
(>=0.6) and say so in its changelog, and that is the mechanism, not the installer.

Everything else in the issue stands — the field is parsed and never checked, and that is
still the thing to fix.

Every plugin carries its own catalogues, and that now includes the release sweep

Core never translates a plugin's strings. Where the plugins actually are:

Repository Languages committed
postulo-paperless, postulo-imap, postulo-apprise, postulo-dav, postulo-helloworld fr_FR, pt_PT (English is the source)
postulo-chromium all 39
postulo-mcp none at all
postulo-firefox none committed — built from the vendored chromium source
postulo-templates none

The five Python plugins are already at exactly the three languages #182 just made the working
standard, which is a happy accident worth making deliberate.

What #182 means for these repositories. Core now translates English, French and European
Portuguese day to day and sweeps up the twenty-four European Union catalogues at a release,
behind uv run pytest -m release. That gate lives in core's suite and cannot see a
plugin's catalogues
— the plugin-locale rule guarantees it. So each official plugin needs
the same gate in its own suite and the same line in its own release checklist, or the rule
quietly means "plugins are never fully translated". postulo-chromium's 39 show the intended
end state; the Python plugins' two show the working state.

postulo-mcp is the one that needs a decision rather than a catalogue. Its user-facing
strings are tool descriptions and prompts read by a model, not a person, and translating
those is at best pointless and at worst harmful — an agent reasoning over a French tool
description while the person writes English. Its README and its command-line messages are a
different matter. Decide which of the two it has, and if the answer is "no catalogue at all",
write that down in the repository so nobody adds one out of consistency.

## Decided: the lock is on the major Not `~=0.3.0` as the issue proposed. What is enforced is the **major**: requires_postulo = ">=0.3,<1.0" A plugin declares the floor it needs and keeps working across core's minors until the next major. The number it carries still tracks the core it was released beside — postulo-mcp 0.3.0 ships with Postulo 0.3.0 — so a person can still pair them at a glance; what changed is that the *installer* no longer refuses a 0.3 plugin on core 0.4. ### One consequence to be aware of, because 0.x is not 1.x Semantic versioning puts every `0.x` in the same major, so a major-only lock during 0.x is a floor and a ceiling at 1.0 and nothing in between. That is genuinely loose while Postulo is pre-1.0, because **0.x is exactly where breaking changes land in the minor** — a plugin written against 0.3's plugin API will install happily on 0.6. That is a reasonable trade while nothing has shipped and both sides move together, and it becomes the right rule the moment Postulo is 1.x. It is worth knowing rather than discovering: a plugin that a core release actually breaks has to raise its own floor (`>=0.6`) and say so in its changelog, and that is the mechanism, not the installer. Everything else in the issue stands — the field is parsed and never checked, and that is still the thing to fix. ## Every plugin carries its own catalogues, and that now includes the release sweep Core never translates a plugin's strings. Where the plugins actually are: | Repository | Languages committed | | --- | --- | | postulo-paperless, postulo-imap, postulo-apprise, postulo-dav, postulo-helloworld | `fr_FR`, `pt_PT` (English is the source) | | postulo-chromium | all 39 | | **postulo-mcp** | **none at all** | | postulo-firefox | none committed — built from the vendored chromium source | | postulo-templates | none | The five Python plugins are already at exactly the three languages #182 just made the working standard, which is a happy accident worth making deliberate. **What #182 means for these repositories.** Core now translates English, French and European Portuguese day to day and sweeps up the twenty-four European Union catalogues at a release, behind `uv run pytest -m release`. That gate lives in core's suite and **cannot see a plugin's catalogues** — the plugin-locale rule guarantees it. So each official plugin needs the same gate in its own suite and the same line in its own release checklist, or the rule quietly means "plugins are never fully translated". postulo-chromium's 39 show the intended end state; the Python plugins' two show the working state. **postulo-mcp is the one that needs a decision rather than a catalogue.** Its user-facing strings are tool descriptions and prompts read by a *model*, not a person, and translating those is at best pointless and at worst harmful — an agent reasoning over a French tool description while the person writes English. Its README and its command-line messages are a different matter. Decide which of the two it has, and if the answer is "no catalogue at all", write that down in the repository so nobody adds one out of consistency.
Author
Owner

Landed across the repositories.

Core — ca99644bc, 6e73da0c5, 3d1f44a36 on main: requires_postulo is the third fatal check, before the download, with a refusal naming both versions; what a release declared is recorded with the plugin and asked again on every visit to Server → Plugins, which marks a plugin that no longer fits for Postulo … and shows a catalogue listing that does not fit its reason instead of an Install button; packaging declared as a dependency; the versioning rule written in CONTRIBUTING.md (Versions of official plugins) and in the wiki's Writing a plugin.

Each official repository took its number, 0.3.0, as decided: postulo-helloworld 9397274, postulo-imap 23cef46, postulo-apprise 57a5e91, postulo-dav b15b164, postulo-paperless 9c05133, postulo-mcp face8f4, postulo-chromium 08774a2 (package.json and the source manifest), postulo-firefox 7d22ee2. postulo-templates says in its README that it carries no version and why (01b8ae2).

postulo-dav and postulo-paperless were also brought to work against the current core on the way (their #1 each), since the pin had to move for the catalogue gate to run at all.

Landed across the repositories. **Core** — `ca99644bc`, `6e73da0c5`, `3d1f44a36` on `main`: `requires_postulo` is the third fatal check, before the download, with a refusal naming both versions; what a release declared is recorded with the plugin and asked again on every visit to *Server → Plugins*, which marks a plugin that no longer fits *for Postulo …* and shows a catalogue listing that does not fit its reason instead of an Install button; `packaging` declared as a dependency; the versioning rule written in `CONTRIBUTING.md` (*Versions of official plugins*) and in the wiki's *Writing a plugin*. **Each official repository took its number**, 0.3.0, as decided: postulo-helloworld `9397274`, postulo-imap `23cef46`, postulo-apprise `57a5e91`, postulo-dav `b15b164`, postulo-paperless `9c05133`, postulo-mcp `face8f4`, postulo-chromium `08774a2` (package.json and the source manifest), postulo-firefox `7d22ee2`. postulo-templates says in its README that it carries no version and why (`01b8ae2`). postulo-dav and postulo-paperless were also brought to work against the current core on the way (their #1 each), since the pin had to move for the catalogue gate to run at all.
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.

Dependencies

No dependencies set.

Reference
Postulo/postulo#186
No description provided.