Installation, versioning and changelog
Installation from the licensed catalog
The marketplace catalog is an allowlist served by the licensing server. Installation accepts a plugin name present in the catalog, not an arbitrary URL or local folder. The administrator opens the plugin marketplace, reviews the manifest metadata and compatibility requirements, and starts installation.
The host fetches the catalog over HTTPS with the installation licence token (revalidated with an ETag,
falling back to the last-good cache when offline). The locally evaluated licence plugin set filters what the
catalog shows: a plugin outside the licence is hidden, and installing or updating it is refused the same way
the server refuses it. Each payload is a tarball pinned by the catalog's sha256 and size, verified while
streaming; the archive listing is validated before extraction (regular files and directories under the plugin
name only, no symlinks or escapes), and the payload manifest name and version must match the catalog entry.
Catalog update status compares the installed manifest version with the validated catalog version, even when
an older payload remains cached. Listing downloads no payload; candidate preparation verifies its exact pin.
After parsing, loader, marketplace staging and update preflight call
validateManifestFolder(manifest, expectedName, directory) in src/plugins/manifest.ts.
It requires the manifest name to equal the logical plugin folder name and returns the
resolved entry path only when it stays lexically within the directory. The expected name is
explicit because a staging directory can have a different basename. This check performs no
filesystem I/O and does not parse the manifest again. A mismatch or escape throws before import
or swap. Marketplace staging separately rejects symlinks and bounds file count and bytes;
preflight separately resolves real paths and requires both plugin and declared service entries
to be files inside the plugin tree. Preflight keeps the injected target-core parser and checks
the effective enabled baseline plus every replacement.
The marketplace gates the downloaded candidate's consent and control dependencies before
queuing the pinned version and sha256. It rejects built-in plugin names, unsafe names,
malformed catalog entries, excessive plugin trees, a newer requiresCore or an
incompatible requiresSharedApi. A missing required control returns the existing dependency
error naming it and available providers. A consent refusal changes no executable files;
confirmation replays the same install or update with the returned acknowledgeGrants.
POST /plugins/marketplace/:name/install with optional enable and
acknowledgeGrants, POST /plugins/marketplace/:name/update with optional
acknowledgeGrants, and POST /plugins/marketplace/:name/repair with an empty body
return 202 { requestId, queued: true }. Install enables by default; enable:false
still uses the same restart and proof transaction but needs no enablement consent.
Any request also advances every other enabled plugin with a newer version in the same run, so a
shared-API bump moves the whole stale set at once; disabled plugins advance when they are enabled.
POST /plugins/marketplace/update with an optional acknowledgeGrants map keyed by plugin name
queues the update of every stale plugin in one request and returns 202 { queued: true, plugins }.
Only the independent update coordinator stages and replaces publisher-plugin code. It shares the core transaction: preflight with the target parser, stop the daemon, web and affected independent plugin sockets/services, record observed backups and the enablement delta, swap, start, prove, then publish receipts or roll the whole set back. Disabled plugins are proven by tree inventory; enabled plugins and their independent service starts must converge. There is no per-plugin pending-install record or daemon-side settlement. Bundled removal persists disablement and soft-removal together with cancellation of queued plugin work inside one plugin mutation lock. User uninstall cancels queued work before awaiting gateway and service shutdown.
Installed publisher identity is the receipt plus an unchanged tree, even when the running
manifest parser rejects it. Such a plugin remains Installed with its receipted version and
degraded reason, and Repair submits an explicit retry. Boot detects missing or unloaded
enabled receipted plugins after registry construction and queues repair without changing
files or appending journal intents. It never repairs customer plugins or queues during an
active non-terminal journal. A version that failed activation is not repaired automatically
again; offline, licence and consent refusals do not consume that activation guard.
Admin GET /system/update is the status authority for the queued request:
{ queue, journal, report }. The journal field is null once the journal reaches a terminal state.
Match the request id against journal operations or the report's
requests and per-plugin requestId; acceptance alone is not proof. Each per-plugin
outcome is one of applied, rolledBack (with a detail), needs-consent, not-licensed,
superseded, failed or skipped. The report state needsOperator marks a result that needs
operator attention. The coordinator rechecks consent and renewed entitlement; a catalog
pin that moved is superseded rather than replaced with different bytes.
Automatic selection plans against the core the transaction installs, or the running source
core for plugin-only runs. Catalog version pins the offered release and must match the payload manifest;
the description is publisher display metadata. A candidate whose requiresCore exceeds the
effective target is deferred as needs-newer-core. Unchanged baseline plugins, including
customer plugins, must parse on the target before services stop.
Versioning
Keep these axes separate:
- The daemon version is the core package version. It is the version compared by
requiresCorefor a manual install; an automatic update compares the core its transaction installs instead. - The plugin's
versionis its own release version. Bump it whenever installed bytes change. apiVersionis the exact breaking plugin API contract.requiresCoreis a minimum core version checked by registry installation.requiresSharedApiis an exact shared-helper contract checked before the entry is imported and during installation.web.requiresApiVersionand the browser registration'srequiresApiVersiontarget the browser runtime contract, currently 54 (PLUGIN_UI_API_VERSIONinpackages/plugin-ui-kit/index.js).
A new hook like brain.run.beforeSettle is additive: plugins that use it raise requiresCore, not
apiVersion (stays "2") and not the browser API (the PLUGIN_UI_API_VERSION axis, unrelated).
Do not use plugin version fields to communicate API compatibility. Update the plugin manifest and registry catalog together.
An artifact patch that only raises the plugin-API floor does not need an in-app note; a release ships one only when something changed for the people using Elowen.
Writing a changelog entry
The in-app What's new page (/p/changelog) is for people using Elowen, not
for developers. Only a release with a big UI change or a new feature, something
a user or an administrator can be shown in the app, gets a short, dated update;
most releases get none. Bug fixes, polish, relabels, speed-ups and technical
detail belong in CHANGELOG.md and the developer docs, never here.
While a release is being developed, write its note as
plugins/changelog/entries/unreleased.md (English) with
unreleased.cs.md and unreleased.sk.md beside it, and commit all three. The
page never shows them. npm run release:prepare produces one entry per calendar
day, in <version>.md, <version>.cs.md and <version>.sk.md. If that day
already has a patch entry, it appends the new content in every language, keeps
the day's title and advances its file name and version to the new patch.
This makes the appended content unread even for readers of the earlier patch.
The product-line overview is separate. It uses the plugin's front-matter parser
and includes all consumed and renamed paths in the release commit. A
release without an unreleased note ships no update; a note missing a
translation, or one that was never committed, stops the release before
anything changes.
---
title: The agent sees what each click did
---
One or two sentences: what this update means for the reader.
### New
- **Short name.** One sentence: what you can do now. Where: [Settings → Models → Model roles](/settings?cat=models)
### Improved
- **Short name.** What works better, faster or more simply, and where.
### Changed
- **Short name.** What behaves differently, or what the reader has to do.
:::admin
- Only administrators receive this part: installation, updates, licences,
plugin compatibility, migrations and backups.
:::
Plugin-specific sections or bullet groups use the same container syntax as administrator notes:
:::plugin sandbox
### Stop a desktop
Stop a desktop from Account while keeping the Project running.
:::
:::admin
:::plugin discord telegram whatsapp
### Voice messages
Configure voice for your connected chat apps.
:::
:::
Names are manifest plugin ids, separated by spaces. Multiple names mean any of those plugins makes the item applicable; the page shows pills only for plugins this account can use. Admin and plugin containers may nest, but plugin containers may not nest inside each other. Use a heading or a bold bullet title inside each plugin container; the page places a small display-name and optional icon pill next to that title, using the shared Badge. Keep the same scopes and audience in cs, sk and en. Untagged core content remains public. The daemon checks the live core access predicate, including installed/loaded, enabled and per-account grants, for admins too. Filtered items never leave the daemon. A note with no remaining content disappears from its listing and unread badge; headings alone are not content. Unclosed containers retain restrictions to the end and log a warning; malformed or stray container markers fail loading.
Every section is optional; keep this order. Name places in the app with the
labels the web dictionaries and plugin strings use, as an in-app link when the
place has a URL (/settings?cat=models, /p/editor) and as a bold click path
otherwise. Do not list plugin versions: write a plugin's change as an ordinary
item in the matching section. Translations are natural Czech and Slovak with
formal address, not word-by-word copies.
The page shows each update by its date and title. Administrators see the
release version as a tooltip on the date; nobody else sees version numbers.
The :::admin container is cut out on the daemon: a non-administrator is never
sent its text, and an update whose only content is administrator notes is not
listed or counted as unread for them. A container left open runs to the end of
the note, so a missing ::: hides text rather than showing it to everybody.
A link to a path inside the app (starting with /) opens through the app's own
navigation; other links behave as ordinary links.
The parser is intentionally a small flat front-matter parser, not a general YAML parser:
- It strips an optional BOM and recognizes a front-matter block only when it
starts with
---, has flatkey: valuelines, and ends with---. - Values in brackets become comma-separated arrays with surrounding quotes
removed. The literals
trueandfalsebecome booleans. Other values are strings with one pair of surrounding quotes removed. versionis required and must be non-empty. Entries without it are skipped with a warning, except theunreleasedfiles, which are skipped silently. Duplicate English versions are skipped.date,title,tags, andpinnedare optional. The parser does not validate the date format.tagsis an array only when the value uses bracket syntax.pinnedis true only for the literaltrue.- The English file owns
date,tags, andpinned. A recognized translation contributes only itstitle, body, administrator section and plugin-scoped items. A translation without an English original is skipped, as is a duplicate translation of the same language and version. Missing translations fall back to English. - Only
csandskfilename suffixes are translations. Other Markdown files are treated as English candidates. - Entries sort with pinned entries first, then by descending numeric version.
Every segment counts and a missing one reads as zero, so the line overview
0.29sorts below the update0.29.54. Text after the first hyphen is ignored, and a segment that does not start with digits sorts as zero. - The body is trimmed Markdown. Unknown front-matter keys are ignored.
- Release assets belong under
entries/assets/<version>/, which the plugin serves at/plugins/changelog/api/asset/<version>/<file>for authenticated users. The asset route accepts onlypng,jpg,jpeg,gif, andwebp, and rejects SVG. Explicitassets/<version>/file.pngreferences keep that asset version when daily release preparation advances the entry's identity. - The page uses the entry version to calculate each account's unread state. Opening the page records the newest visible update, so every later update is new to that account. An admin can reset an update to unread for all accounts.
Owning code and limits
- Parser and visibility rules:
plugins/changelog/lib/entries.mjs. Routes, asset serving and the per-account marker:plugins/changelog/lib/api.mjs. Release preparation:scripts/release-prepare.mjs, which runs the same parser for the day's merge. - Minimal use: write
unreleased.mdwith itscsandsktranslations and commit all three. The release procedure then turns them into the day's entry; no other plugin code is involved. - Absence: a release without a note ships no update, so the page lists no entry for that release.
- Limits: the parser is a flat front-matter reader, not YAML. Container markers must be exact, and a malformed one stops loading. Assets are limited to the five raster types above.
- Cost: entries are parsed once per boot, so a note change needs a daemon restart, and the page reads the account's marker only.