A plugin carries a logo — served by Postulo, raster, and not necessarily somebody else's trademark #106

Closed
opened 2026-09-07 16:33:24 +00:00 by tiagoagueda · 2 comments
Owner

Observation

plugins also must include a logo, in this case use the oficial europass logo

Extends #97, which gives a plugin a short name, a full name, an author, a version, a
description and a source link. A logo is the seventh, and unlike the other six it is not a
string -- it needs somewhere to live, a way to be served, and a decision about one format.

It cannot be a static file

Plugins are installed at runtime onto the data volume. collectstatic ran when the image
was built, and production serves through CompressedManifestStaticFilesStorage, which
raises on a file the manifest never learned rather than returning a dead link. So a logo
shipped inside a plugin wheel is invisible to the static machinery, always. This is the same
wall #88 hit: static files are served under a content hash, and there is no pattern a name
can be built from.

So it is a view: the bytes come out of the installed package, served by Postulo, under a URL
carrying the plugin's name.

And it cannot be a URL either

jobs/logos.py has already settled this for company logos, and the reasoning transfers
exactly:

The production policy is img-src 'self', and that is not an obstacle to work around --
it is the reason this module exists. An <img> pointing at somebody else's server would
tell them, on every page view, which companies this person is applying to and when they
looked.

A plugin logo fetched from a vendor's CDN would tell that vendor which instances run their
plugin, how many people use it, and when. Same answer: Postulo serves it or it is not shown.

The format question, which this project has already answered once

Also from jobs/logos.py:

raster only for now: PNG, JPEG, GIF and WebP. SVG is the format logos most often come
in and the one that needs care -- it can carry scripts and references to other files, and a
direct visit to the file is not the <img> context where a browser refuses to run them.
Accepting SVG means a sanitiser, and that is its own step.

That applies more strongly here, not less. A company logo is fetched from a web page; a
plugin logo arrives inside code an administrator installed, served from Postulo's own origin
under Postulo's own session. An SVG with a script in it, opened directly, is same-origin
script execution -- which is exactly what the content security policy exists to prevent
everywhere else.

Three ways, in order of how much they cost:

  1. Raster only, decoded and re-encoded like a company logo already is, which drops
    whatever metadata the file carried and caps its size. Consistent with the decision
    already made, and needs no new thinking. Recommended.
  2. SVG behind a sanitiser, plus Content-Security-Policy: sandbox and an explicit
    Content-Type on the response. Better-looking at every size and, as logos.py says,
    its own step.
  3. SVG served as it arrived. No.

Scope

  • A logo on the plugin, beside the identity fields from #97: a filename inside the
    package rather than a path, so nothing a plugin declares can become a path.
  • A view serving it, owner-agnostic but administrator-only where the plugin is not
    available to the person asking, with a cap on bytes and dimensions and a re-encode.
  • A fallback for a plugin with no logo -- the initials tile the interface already uses for a
    person and for a company with no logo, so nothing is a broken image.
  • The built-ins get one: the four in #98, and Europass from #99.

The Europass logo is somebody else's trademark, and that needs deciding rather than

downloading

The Europass brand belongs to the European Union. Two things worth separating:

Displaying it to say "this reads Europass files" is nominative use -- naming a format by
its own mark -- and is what every integration directory does. Uncontroversial.

Shipping the file in this repository is a different statement. Postulo is AGPL-3.0, and
that licence grants rights to the code: it cannot sublicense a mark the project does not
own. Every fork would be redistributing an EU trademark under a licence that has nothing to
say about it. And the Commission's own reuse decision, which makes its documents freely
reusable, excludes logos and trademarks from its scope -- so "it is an EU document" is
not the answer here.

Three options, and this is a decision for the maintainer rather than a detail for whoever
implements it. Not legal advice, just the shape of it:

  1. Ship it with its own notice, in its own directory, saying the mark belongs to the
    European Union and is used to identify the format rather than to claim endorsement.
    This is exactly what the project already does for artwork it did not write:
    src/postulo/static/flags/LICENSE.txt sits beside the flags because MIT asks that the
    notice travel with the files. Same shape, different reason.
  2. Do not ship it. The internal importer gets a neutral document mark, and its full name
    -- "Europass" -- does the identifying. Costs nothing legally and a little recognisably.
  3. Ask. Europass has a contact address and the answer would settle it for every downstream
    fork rather than for this repository only.

Option 1 is the common practice and option 2 is the one that cannot go wrong. Worth noting
that whichever is chosen sets the precedent for every future plugin: a Paperless logo, a
Telegram logo, a Nextcloud logo. A rule that only works for logos Postulo happens to like is
not a rule.

Classification

Enhancement, interface. Extends #97. The trademark question is not blocking -- a plugin with
no logo must render properly anyway, so the machinery can land before the artwork does.

## Observation > plugins also must include a logo, in this case use the oficial europass logo Extends #97, which gives a plugin a short name, a full name, an author, a version, a description and a source link. A logo is the seventh, and unlike the other six it is not a string -- it needs somewhere to live, a way to be served, and a decision about one format. ## It cannot be a static file Plugins are installed at runtime onto the data volume. `collectstatic` ran when the image was built, and production serves through `CompressedManifestStaticFilesStorage`, which raises on a file the manifest never learned rather than returning a dead link. So a logo shipped inside a plugin wheel is invisible to the static machinery, always. This is the same wall #88 hit: static files are served under a content hash, and there is no pattern a name can be built from. So it is a view: the bytes come out of the installed package, served by Postulo, under a URL carrying the plugin's name. ## And it cannot be a URL either `jobs/logos.py` has already settled this for company logos, and the reasoning transfers exactly: > The production policy is `img-src 'self'`, and that is not an obstacle to work around -- > it is the reason this module exists. An `<img>` pointing at somebody else's server would > tell them, on every page view, which companies this person is applying to and when they > looked. A plugin logo fetched from a vendor's CDN would tell that vendor which instances run their plugin, how many people use it, and when. Same answer: Postulo serves it or it is not shown. ## The format question, which this project has already answered once Also from `jobs/logos.py`: > **raster only** for now: PNG, JPEG, GIF and WebP. SVG is the format logos most often come > in and the one that needs care -- it can carry scripts and references to other files, and a > direct visit to the file is not the `<img>` context where a browser refuses to run them. > Accepting SVG means a sanitiser, and that is its own step. **That applies more strongly here, not less.** A company logo is fetched from a web page; a plugin logo arrives inside code an administrator installed, served from Postulo's own origin under Postulo's own session. An SVG with a script in it, opened directly, is same-origin script execution -- which is exactly what the content security policy exists to prevent everywhere else. Three ways, in order of how much they cost: 1. **Raster only**, decoded and re-encoded like a company logo already is, which drops whatever metadata the file carried and caps its size. Consistent with the decision already made, and needs no new thinking. **Recommended.** 2. SVG behind a sanitiser, plus `Content-Security-Policy: sandbox` and an explicit `Content-Type` on the response. Better-looking at every size and, as `logos.py` says, its own step. 3. SVG served as it arrived. No. ## Scope - A `logo` on the plugin, beside the identity fields from #97: a filename inside the package rather than a path, so nothing a plugin declares can become a path. - A view serving it, owner-agnostic but administrator-only where the plugin is not available to the person asking, with a cap on bytes and dimensions and a re-encode. - A fallback for a plugin with no logo -- the initials tile the interface already uses for a person and for a company with no logo, so nothing is a broken image. - The built-ins get one: the four in #98, and Europass from #99. ## The Europass logo is somebody else's trademark, and that needs deciding rather than downloading The Europass brand belongs to the European Union. Two things worth separating: **Displaying it** to say "this reads Europass files" is nominative use -- naming a format by its own mark -- and is what every integration directory does. Uncontroversial. **Shipping the file in this repository** is a different statement. Postulo is AGPL-3.0, and that licence grants rights to the *code*: it cannot sublicense a mark the project does not own. Every fork would be redistributing an EU trademark under a licence that has nothing to say about it. And the Commission's own reuse decision, which makes its documents freely reusable, **excludes logos and trademarks from its scope** -- so "it is an EU document" is not the answer here. Three options, and this is a decision for the maintainer rather than a detail for whoever implements it. Not legal advice, just the shape of it: 1. **Ship it with its own notice**, in its own directory, saying the mark belongs to the European Union and is used to identify the format rather than to claim endorsement. This is exactly what the project already does for artwork it did not write: `src/postulo/static/flags/LICENSE.txt` sits beside the flags because MIT asks that the notice travel with the files. Same shape, different reason. 2. **Do not ship it.** The internal importer gets a neutral document mark, and its full name -- "Europass" -- does the identifying. Costs nothing legally and a little recognisably. 3. Ask. Europass has a contact address and the answer would settle it for every downstream fork rather than for this repository only. Option 1 is the common practice and option 2 is the one that cannot go wrong. Worth noting that whichever is chosen sets the precedent for **every future plugin**: a Paperless logo, a Telegram logo, a Nextcloud logo. A rule that only works for logos Postulo happens to like is not a rule. ## Classification Enhancement, interface. Extends #97. The trademark question is not blocking -- a plugin with no logo must render properly anyway, so the machinery can land before the artwork does.
tiagoagueda added this to the 0.3.0 milestone 2026-09-07 16:33:24 +00:00
Author
Owner

Decided — option 1

Ship the Europass logo, with its own notice beside it, in its own place: the mark belongs to
the European Union, it is used to identify the format Postulo reads, and no endorsement is
claimed.

The shape already exists in this repository. src/postulo/static/flags/LICENSE.txt sits
beside the flag artwork because MIT asks that the notice travel with the files. Same
arrangement, different reason — MIT asks; a trademark is not licensed at all, and the notice
is what makes the use legible rather than what satisfies a licence.

This is a rule, not an exception

Written down now because the next one will not be Europass. It will be Paperless, or
Telegram, or Nextcloud, and a rule that only works for the mark that prompted it is not a
rule.

A plugin may ship a third party's mark when all four hold:

  1. The mark identifies something the plugin actually works with — the format it reads, the
    service it connects to. Never decoration, never a badge of quality.
  2. The mark is unmodified: not recoloured, not redrawn, not composed into something else.
    Postulo has opinions about its own palette and none about anybody else's.
  3. A notice travels with the file, naming the owner and stating that no endorsement is
    claimed or implied.
  4. It sits outside the AGPL grant, in its own directory, with that stated — because the
    licence covers the code and cannot speak for somebody else's mark, and every fork
    redistributes whatever is in the tree.

Where any of the four fails, the plugin gets the neutral fallback and its name does the work.

What the implementation carries

  • The logo in its own directory with a notice file, following the flags/LICENSE.txt
    pattern already in the tree.
  • A TRADEMARKS.md at the root, or a section of the README beside the existing licence
    notes — the README already names Lucide (ISC) and flag-icons (MIT), and this is a third
    kind of thing: artwork under nobody's licence, used nominatively. It should read
    differently from the two above it, because it is different.
  • Raster, per the format decision in the issue body: PNG at the sizes the interface uses.

One interaction worth catching before it happens

#94 puts an Official badge on plugins whose provenance verifies against a signed index.
#106 puts a vendor's logo on a plugin. Together, on one row, those say something neither
was meant to say: that the vendor has blessed the plugin.

Postulo's own catalogue signature means this file is the one we published. It does not mean
Europass, Telegram or anybody else has looked at it. So the two should not share a visual
frame — a logo is the plugin's identity and a badge is Postulo's claim about provenance, and
they should read as coming from different places. Worth deciding in #94's design rather than
discovering on the page.

## Decided — option 1 Ship the Europass logo, with its own notice beside it, in its own place: the mark belongs to the European Union, it is used to identify the format Postulo reads, and no endorsement is claimed. The shape already exists in this repository. `src/postulo/static/flags/LICENSE.txt` sits beside the flag artwork because MIT asks that the notice travel with the files. Same arrangement, different reason — MIT asks; a trademark is not licensed at all, and the notice is what makes the use legible rather than what satisfies a licence. ## This is a rule, not an exception Written down now because the next one will not be Europass. It will be Paperless, or Telegram, or Nextcloud, and a rule that only works for the mark that prompted it is not a rule. **A plugin may ship a third party's mark when all four hold:** 1. The mark identifies something the plugin actually works with — the format it reads, the service it connects to. Never decoration, never a badge of quality. 2. The mark is unmodified: not recoloured, not redrawn, not composed into something else. Postulo has opinions about its own palette and none about anybody else's. 3. A notice travels with the file, naming the owner and stating that no endorsement is claimed or implied. 4. It sits outside the AGPL grant, in its own directory, with that stated — because the licence covers the code and cannot speak for somebody else's mark, and every fork redistributes whatever is in the tree. Where any of the four fails, the plugin gets the neutral fallback and its name does the work. ## What the implementation carries - The logo in its own directory with a notice file, following the `flags/LICENSE.txt` pattern already in the tree. - A `TRADEMARKS.md` at the root, or a section of the README beside the existing licence notes — the README already names Lucide (ISC) and flag-icons (MIT), and this is a third kind of thing: artwork under nobody's licence, used nominatively. It should read differently from the two above it, because it is different. - Raster, per the format decision in the issue body: PNG at the sizes the interface uses. ## One interaction worth catching before it happens #94 puts an **Official** badge on plugins whose provenance verifies against a signed index. #106 puts a **vendor's logo** on a plugin. Together, on one row, those say something neither was meant to say: that the vendor has blessed the plugin. Postulo's own catalogue signature means *this file is the one we published*. It does not mean Europass, Telegram or anybody else has looked at it. So the two should not share a visual frame — a logo is the plugin's identity and a badge is Postulo's claim about provenance, and they should read as coming from different places. Worth deciding in #94's design rather than discovering on the page.
Author
Owner

The logo field has been on the manifest since #97 with nothing rendering it. It renders
now: a plugin names a file inside its own package and Postulo serves it beside the plugin's
name.

All three constraints the issue set out are the ones that shaped it, and each had been
decided once already elsewhere in this codebase:

  • Not a static file. Plugins are installed at run time; collectstatic ran at build
    time; the manifest storage raises on a file it never learned. The wall #88 hit. So a view.
  • Not a URL. jobs/logos.py settled it — an <img> at the plugin author's server would
    tell them which instances run their code, how many people use it and when. img-src 'self'
    is the reason, not the obstacle.
  • Raster only, and re-encoded. What is served is a 256-pixel PNG Postulo produced from
    what the plugin shipped, not the plugin's file passed through — which also settles "a valid
    image with something appended" without having to reason about it.

A name, never a path. A separator or a leading dot is refused rather than normalised, and
importlib.resources reads from the package, so nothing a plugin declares can name a file
outside it. Four parametrised cases hold that.

The path with the most care in it is the one where there is nothing to show, because
that is the ordinary case. No logo, a missing file, an oversized file, an SVG: all four give
the initials tile the interface already uses for a person with no picture and a company with
no logo — one answer to "there is no image here", now in three places — with the reason in
the log rather than a broken image beside a name.

The trademark question, decided

The issue asked for a decision and said it sets the precedent for every future plugin. Option
2: Postulo ships no logo for any plugin of its own
, and the rule is in docs/PLUGINS.md
rather than settled logo by logo.

Displaying a mark to say "this reads Europass files" is nominative use and is not in
question. Redistributing the file under AGPL-3.0 is the different statement: that licence
grants rights to the code and cannot sublicense a mark the project does not own, so every
fork would be redistributing an EU trademark under a licence with nothing to say about it —
and the Commission's reuse decision excludes logos and trademarks from its scope, so "it
is an EU document" is not an answer.

A rule that only works for the marks a project happens to like is not a rule, so it is
written as a rule. Where an owner does permit redistribution, the shape to copy is named:
src/postulo/static/flags/LICENSE.txt, where the notice travels with the files. Option 3 —
asking Europass — stays open and would settle it for every downstream fork rather than for
this repository; nothing here forecloses it.

The consequence is that the built-ins show initials tiles today, which is exactly the
fallback the issue said had to work anyway.

Small things

The four failure messages are English and stay English: every one reaches a log line an
administrator reads, none is ever rendered to somebody using Postulo, and thirty-nine
catalogues of them would be work for text no reader sees.

connections:logo is recorded in tests/test_page_coverage.py as bytes rather than a page,
with the reason — that test caught it, which is what it is for.

15 tests. Shipped in 2325e46 on 0.3.0, with main kept level. #94 is the other half of
the plugins page and is unaffected.

The `logo` field has been on the manifest since #97 with nothing rendering it. It renders now: a plugin names a file inside its own package and Postulo serves it beside the plugin's name. **All three constraints the issue set out are the ones that shaped it**, and each had been decided once already elsewhere in this codebase: - **Not a static file.** Plugins are installed at run time; `collectstatic` ran at build time; the manifest storage raises on a file it never learned. The wall #88 hit. So a view. - **Not a URL.** `jobs/logos.py` settled it — an `<img>` at the plugin author's server would tell them which instances run their code, how many people use it and when. `img-src 'self'` is the reason, not the obstacle. - **Raster only**, and re-encoded. What is served is a 256-pixel PNG Postulo produced from what the plugin shipped, not the plugin's file passed through — which also settles "a valid image with something appended" without having to reason about it. **A name, never a path.** A separator or a leading dot is refused rather than normalised, and `importlib.resources` reads from the package, so nothing a plugin declares can name a file outside it. Four parametrised cases hold that. **The path with the most care in it is the one where there is nothing to show**, because that is the ordinary case. No logo, a missing file, an oversized file, an SVG: all four give the initials tile the interface already uses for a person with no picture and a company with no logo — one answer to "there is no image here", now in three places — with the reason in the log rather than a broken image beside a name. ## The trademark question, decided The issue asked for a decision and said it sets the precedent for every future plugin. **Option 2: Postulo ships no logo for any plugin of its own**, and the rule is in `docs/PLUGINS.md` rather than settled logo by logo. Displaying a mark to say "this reads Europass files" is nominative use and is not in question. *Redistributing the file* under AGPL-3.0 is the different statement: that licence grants rights to the code and cannot sublicense a mark the project does not own, so every fork would be redistributing an EU trademark under a licence with nothing to say about it — and the Commission's reuse decision **excludes logos and trademarks from its scope**, so "it is an EU document" is not an answer. A rule that only works for the marks a project happens to like is not a rule, so it is written as a rule. Where an owner does permit redistribution, the shape to copy is named: `src/postulo/static/flags/LICENSE.txt`, where the notice travels with the files. Option 3 — asking Europass — stays open and would settle it for every downstream fork rather than for this repository; nothing here forecloses it. The consequence is that the built-ins show initials tiles today, which is exactly the fallback the issue said had to work anyway. ## Small things The four failure messages are English and stay English: every one reaches a log line an administrator reads, none is ever rendered to somebody using Postulo, and thirty-nine catalogues of them would be work for text no reader sees. `connections:logo` is recorded in `tests/test_page_coverage.py` as bytes rather than a page, with the reason — that test caught it, which is what it is for. 15 tests. Shipped in `2325e46` on `0.3.0`, with `main` kept level. #94 is the other half of the plugins page and is unaffected.
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#106
No description provided.