Plugin install docs: the image-on-top example has no lock constraint, and one installs an unverified PyPI name #542

Closed
opened 2026-10-01 10:47:30 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d and the wiki at de1f52467, comparing the plugin install instructions with docker/Dockerfile.

Related to #61.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

Both pages that explain baking a plugin into the image give two routes — the POSTULO_EXTRA_PACKAGES build argument and a Dockerfile FROM Postulo's published image — and then say that either way the plugin is installed with Postulo's lock as a constraint. Only the build argument does that. The FROM example runs a bare uv pip install as root in the runtime image, which carries no lock, so a plugin's requirements are free to move Django, allauth, httpx or anything else Postulo pins. Writing a plugin also installs the bare name postulo-apprise, which resolves on PyPI, where Postulo's plugins are not published.

  • Where: postulo.wiki/Installing-Postulo.md:137-148; postulo.wiki/Writing-a-plugin.md:1528-1537; docker/Dockerfile:119-128 (the constraint, build stage only) and :264-269 in the runtime stage ("No lock, no uv cache": only .venv, src and manage.py are copied).
  • What happens:
    1. An operator who would rather not build from source copies the FROM source.tiagoagueda.com/postulo/postulo:0.2 example. uv pip install --no-cache <plugin> resolves the plugin's requirements with nothing holding the environment's pins, so a loose django>=… or httpx>=… in a plugin upgrades a core dependency to a version Postulo was never tested with. The page promised the opposite.
    2. Copying Writing a plugin's version instead installs whatever PyPI serves under postulo-apprise. The official plugin lives at git+https://source.tiagoagueda.com/postulo/postulo-apprise.git (its own README, and Installing Postulo on the same step); Writing a plugin line 382 says Postulo itself is not on PyPI. Whoever registers that name there decides what runs, as root at build time and inside Postulo's process afterwards.
    3. The Installing Postulo variant uses the git URL, but the runtime stage installs no git (docker/Dockerfile:244-260, the runtime apt-get install list), and uv needs the git executable for a git+https requirement, so as written that example is likely to fail at build.
  • Who can trigger it: anyone able to publish the package name postulo-apprise (or any other official plugin name) on PyPI, against an operator who follows Writing a plugin; and any plugin with loose requirements, against an operator who follows either FROM example.
  • Why it matters: the "cannot change the version of anything Postulo pins" promise is the protection that keeps a plugin from silently swapping a security-relevant dependency, and the bare name is the dependency-confusion class of weakness: trust placed in a name on an index the project does not publish to.
  • Fix: in the build stage, keep the exported file (for example COPY --from=build /tmp/constraints.txt /app/constraints.txt, exporting it unconditionally), or generate one at install time from the running environment as plugins/installing.py's constraints() already does for uploads. Show uv pip install --no-cache --constraint /app/constraints.txt git+https://… in both FROM examples, with apt-get install git before it (or a wheel URL instead of git+https). Replace the bare postulo-apprise with the git URL. If none of that is done, drop the constraint sentence from the FROM route.

Done when

  • A test in tests/test_image_build.py fails while the runtime stage ships no constraints file, and passes once /app/constraints.txt (or its equivalent) is in the image.
  • Both wiki examples name the constraint and the git URL, and neither installs a bare package name.
Found on 2026-09-29 in a read-only review of postulo at 95348908d and the wiki at de1f52467, comparing the plugin install instructions with `docker/Dockerfile`. Related to #61. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. Both pages that explain baking a plugin into the image give two routes — the `POSTULO_EXTRA_PACKAGES` build argument and a Dockerfile `FROM` Postulo's published image — and then say that either way the plugin is installed with Postulo's lock as a constraint. Only the build argument does that. The `FROM` example runs a bare `uv pip install` as root in the runtime image, which carries no lock, so a plugin's requirements are free to move Django, allauth, httpx or anything else Postulo pins. *Writing a plugin* also installs the bare name `postulo-apprise`, which resolves on PyPI, where Postulo's plugins are not published. - **Where:** `postulo.wiki/Installing-Postulo.md:137-148`; `postulo.wiki/Writing-a-plugin.md:1528-1537`; `docker/Dockerfile:119-128` (the constraint, build stage only) and `:264-269` in the runtime stage ("No lock, no uv cache": only `.venv`, `src` and `manage.py` are copied). - **What happens:** 1. An operator who would rather not build from source copies the `FROM source.tiagoagueda.com/postulo/postulo:0.2` example. `uv pip install --no-cache <plugin>` resolves the plugin's requirements with nothing holding the environment's pins, so a loose `django>=…` or `httpx>=…` in a plugin upgrades a core dependency to a version Postulo was never tested with. The page promised the opposite. 2. Copying *Writing a plugin*'s version instead installs whatever PyPI serves under `postulo-apprise`. The official plugin lives at `git+https://source.tiagoagueda.com/postulo/postulo-apprise.git` (its own README, and *Installing Postulo* on the same step); *Writing a plugin* line 382 says Postulo itself is not on PyPI. Whoever registers that name there decides what runs, as root at build time and inside Postulo's process afterwards. 3. The *Installing Postulo* variant uses the git URL, but the runtime stage installs no `git` (`docker/Dockerfile:244-260`, the runtime `apt-get install` list), and uv needs the `git` executable for a `git+https` requirement, so as written that example is likely to fail at build. - **Who can trigger it:** anyone able to publish the package name `postulo-apprise` (or any other official plugin name) on PyPI, against an operator who follows *Writing a plugin*; and any plugin with loose requirements, against an operator who follows either `FROM` example. - **Why it matters:** the "cannot change the version of anything Postulo pins" promise is the protection that keeps a plugin from silently swapping a security-relevant dependency, and the bare name is the dependency-confusion class of weakness: trust placed in a name on an index the project does not publish to. - **Fix:** in the build stage, keep the exported file (for example `COPY --from=build /tmp/constraints.txt /app/constraints.txt`, exporting it unconditionally), or generate one at install time from the running environment as `plugins/installing.py`'s `constraints()` already does for uploads. Show `uv pip install --no-cache --constraint /app/constraints.txt git+https://…` in both `FROM` examples, with `apt-get install git` before it (or a wheel URL instead of `git+https`). Replace the bare `postulo-apprise` with the git URL. If none of that is done, drop the constraint sentence from the `FROM` route. ## Done when - A test in `tests/test_image_build.py` fails while the runtime stage ships no constraints file, and passes once `/app/constraints.txt` (or its equivalent) is in the image. - Both wiki examples name the constraint and the git URL, and neither installs a bare package name.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:30 +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.

Dependencies

No dependencies set

Reference
Postulo/postulo#542
No description provided.