Extensions read as two kinds today, and the division is not in the engine: they already share one directory under the profile, one discovery pass and one enable/disable switch. The split is imposed in four places, and this writes down where and what each costs to remove. The load-bearing one is that removal cannot stick. Seeding copies back any pre-installed extension whose folder is missing, so deleting one is undone on the next launch — which is why the UI marks them unremovable, and why there is no uninstall action for anything at all. A tombstone list fixes both. The urgent one is that the two publishing channels have one install path between them. Everything installs through the community catalogue; the operator channel has no index and no install route. So the Install buttons on the shipped extensions' cards do not work, and moving an extension off the community channel makes it uninstallable rather than better distributed. Also records what not to flatten: one catalogue and one lifecycle, but the signer stays visible, because an extension signed by the operator key and one signed by a stranger's wallet get the same access once installed.
7.2 KiB
One extension directory
Extensions are currently presented as two kinds. They should be one kind with different provenance. This is what that costs and where the seams actually are.
The goal, stated plainly: one catalogue, one lifecycle. Every extension can be installed, removed, turned on and turned off by the same actions. Some arrive already installed. Who wrote it and who signed it is a fact about an extension — searchable, shown on its card — not a wall between two lists.
The good news: the runtime is already unified
None of the split is in the engine.
- Every extension lives in one directory,
<userData>/extensions/. A pre-installed one and a downloaded one land in the same place, side by side.seedBundledAddons()even says so: "the framework treats bundled and user add-ons identically, no special path". - One discovery pass activates them all —
AddonHost.discoverAndActivate()scans that single directory. - Enable and disable are already uniform:
settings.disabledAddonsis a list of ids, andaddons-set-enableddoes not care where an extension came from.
So this is not a rewrite. It is removing four specific things that impose a division the engine never had.
Seam 1 — removal does not stick, so the UI hides it
seedBundledAddons() copies any bundled folder whose target is missing. Delete
a pre-installed extension and it is back on the next launch. That single fact
is why the UI marks them unremovable, and the comment on addons-list admits
the motive: "Mark bundled add-ons so the extensions UI can render them
differently… look built-in rather than removable extensions."
Worse, there is no uninstall at all — for anything. The only removal route is
addons-reveal, opening the folder in the file manager, described in the
source as "the primary way users edit / uninstall add-ons".
Fix. A tombstone list, settings.removedExtensions: string[].
- Seeding skips any id on it.
- Uninstall adds the id; installing again clears it.
Then removal means something for every extension, and a user who removes a pre-installed one does not have it resurrected by the next update.
Fix. An extensions-remove handler that deactivates the extension, moves
its folder into the existing extensions-backups/<id>-<version>-<stamp>/
rather than deleting outright, tombstones the id and re-runs discovery. Its
stored data in extensions-data/<id>.json survives by default so a reinstall
keeps settings, with an explicit "also delete its data" option for people who
mean it.
Seam 2 — two catalogues, and one of them cannot install
There are two publishing shapes today:
| Where | Signed by | Indexed? | Installable? | |
|---|---|---|---|---|
| Operator channel | bns/theseus/extensions/<id>/ |
operator Ed25519 key | no index, per-id only | no |
| Community | bns/theseus/extensions/community/<id>/ |
publisher's BCH key, checked against the name's on-chain owner | /api/ext/catalog |
yes |
Every install route — the Settings button and the
theseus://extensions/install/<id> deep link — goes through
installCommunityById, which resolves the id against the community catalogue.
main.js has no operator-channel install path at all.
Two consequences, both live right now:
- The Install in Theseus buttons added to the Screenshot and PDF Editor cards do not work. Those ids are not in the community catalogue. It goes unnoticed only because both already ship in the build.
- Moving an extension from the community channel to the operator channel makes it uninstallable unless it also ships in the build. This is exactly the trap the Word editor migration is walking into.
Fix. One catalogue endpoint listing everything, with the signing scheme as
a field rather than as a separate world. /api/ext/catalog keeps its URL and
merges both prefixes, each entry carrying:
{
"id": "pdf-editor",
"publisher": "Silent Mode", // display name of the developer
"signer": { "kind": "operator" }, // or { kind: "name", name: "theseus.x", owner: "bchtest:…" }
"channel": "https://…/extensions/pdf-editor/updates.json",
"latest": "0.2.0",
"sha256": "…"
}
and one installExtension(id) in main.js that resolves from it and verifies
with whichever key the entry declares. That is the smallest change that makes
installation uniform, and it repairs the dead buttons on the way past.
Seam 3 — two lists in Settings
Settings has an Extensions section and a separate Plug-ins section, divided by
manifest.category === "plugin" so that Aegis and friends "look built-in".
That is the same division again, by a different field.
Fix. One list. Category becomes a filter value, not a page. Alongside it:
- free-text search over name, description and developer
- filter chips: All · Enabled · Disabled · Pre-installed · by developer
- each card shows developer, version, where it updates from, and the same four actions for every row
Seam 4 — the website mirrors the split
site-theseus-x/extensions/index.html hardcodes a card per shipped extension
and renders community ones from catalog.json into a separate section below.
Adding an extension means hand-writing HTML.
Fix. Render every card from the one catalogue. The page already has the
machinery — it fills version, SHA-256 and tarball URL live from each
updates.json — it just needs one loop over one list instead of two.
The record an extension carries
AddonHost.snapshot() reports id, name, version, description, author, icon,
capabilities, category, folder, enabled and error. main.js then bolts on
bundled by looking for a same-named folder in resources/bundled-addons/.
Replace that flag and add the provenance the new UI wants to search on:
publisher— who signed the channelorigin—"preinstalled"or"installed", a badge and nothing morechannel— itsupdates.json, so a card can say where updates come frominstalledAt,updatedAtremovable— true for everything, once tombstones exist
bundled goes away. Nothing downstream should branch on where an extension
came from.
One thing not to flatten
"Who the developer is" should not split the catalogue, and it should not be hidden either. An extension signed by the Silent Mode operator key and one signed by a stranger's wallet carry the same permissions once installed, and they are not equally safe. The consent dialog already gets this right — it names the publisher and says the extension "runs with the same access as any add-on". One list, one lifecycle, and the signer stated plainly on the card and in the prompt.
Order of work
Each step stands on its own and leaves the product working.
- Tombstones and
extensions-remove. Removal becomes real for every extension. No UI change yet. - One catalogue and one
installExtension. Fixes the dead Install buttons and unblocks the Word editor migration. - One Settings list with search and filters; Plug-ins becomes a filter.
- Site page renders from the catalogue instead of hardcoded cards.
- Drop
bundledfromaddons-listand the last branches on it.
Step 2 is the one with a deadline attached: until it lands, nothing should be removed from the community catalogue, because that is the only route by which an extension can be installed.