The mark now shares pdf-editor's silhouette, corner geometry and weight, in blue against its red, so the two editors read as a pair in the dock. It carries a pilcrow rather than a format label: at 16px "DOC" is a smudge while the paragraph mark is still a glyph, and it says word processor rather than file extension. Drawn as paths, not <text>. An icon that needs a particular font installed to make sense is an icon that eventually renders wrong somewhere. Still nothing of Microsoft's: their blue sheet carries a white W, and the page-with-a-folded-corner is the universal document glyph rather than anyone's property. Also adds docs/DESIGN-one-extension-catalogue.md, which writes down the larger point this kept bumping into — that bundled and community extensions are two systems for one kind of thing, with two trust rules and two lists in Settings, and that "ships by default" should be a line of configuration rather than a separate distribution path. Proposed only; the awkward part is keeping a fresh offline install usable, which the note answers by treating the shipped copies as a pre-seeded cache that still verifies like everything else.
4.6 KiB
One catalogue, one set of rules
Status: proposed, 2026-09-22. Not implemented.
Theseus currently runs two extension systems that happen to produce the same kind of thing.
| Bundled add-ons | Community extensions | |
|---|---|---|
| How it arrives | compiled into the build, seeded into the profile on first run | installed from the gateway catalogue |
| Who signs | the Silent Mode operator's Ed25519 key, pubkeys compiled into the app | the owner of a BNS name, checked against the chain |
| Update channel | theseus.x/extensions/<id>/updates.json |
theseus.x/extensions/community/<id>/updates.json |
| In Settings | a section headed "Built into Theseus" | a separate section headed "Community" |
| How to publish | scripts/sign-addon-update.mjs + sia-upload |
the publish page, or PUT /api/ext/… |
Two catalogues in the UI, two trust rules, two publishing paths — for a difference that is really only "who wrote it" and "is it there on day one".
What this should be instead
One catalogue. Every extension is listed in the same place, whoever wrote it. Silent Mode publishes under a Silent Mode name the same way anyone else publishes under theirs. Being first-party is a fact about the publisher, not a separate distribution system, and the UI should say so with a line of text rather than a second list.
One rule for trust. An extension is installable because a signature
checks out against the current owner of the publisher's name. That is
already what verifyPublisherEntry does; the operator Ed25519 key becomes
either one more accepted signer or nothing at all. pickChannelEntry
already accepts both, so this is mostly deletion.
Defaults are configuration. Which extensions a fresh Theseus starts with is a list of ids in the app's config, not a folder that gets copied. Changing the default set should be editing that list, not moving directories around and rebuilding.
The one thing that needs care
If defaults are fetched from the catalogue on first run, a fresh install with no network — or with the gateway down — has no editor, no wallet and no screenshot tool. That is a real regression from today, where the build carries them.
So keep shipping the code, but stop treating it as a different kind of thing:
- The build continues to include the default extensions' folders. They are a pre-seeded cache, not a separate class of add-on.
- On first run they are installed from that cache, then verified and updated against the catalogue exactly like anything the user installed by hand.
- If a seeded copy fails verification, it is quarantined rather than run — a cache is only a cache.
That keeps offline first-run working while leaving one set of rules everywhere else.
Sketch of the work
- Config:
defaultExtensions: [{ id, publisher }]in the app's settings, read at first run and on profile migration. Ids the user has removed are remembered and not re-seeded — uninstalling a default must stick. main.js: seeding installs through the normal install path instead of copying a folder and calling it built-in. One code path, one place where a signature is checked.settings.html: one list. "Installed by default" becomes a badge on a row, next to the publisher's name. The separate Community section and its Install button fold into the single catalogue view.addon-updater.js: drop the distinction between the two channel shapes; an extension has a publisher and an updates URL.- Gateway: the catalogue gains first-party entries. Nothing changes
structurally —
/api/extalready stores whatever a name owner signs. - Publishing:
scripts/sign-addon-update.mjsand the operator key stop being the first-party route. Silent Mode's own extensions go throughPUT /api/ext/<our-name>/<id>/<version>like everyone else's, which means the release process needs that name's wallet rather than a key file.
Open questions
- Which name publishes Silent Mode's extensions? Whoever goes first owns each id permanently. Worth deciding once, deliberately.
- Pinning. A default extension is installed without anyone choosing it, so a compromised or coerced catalogue entry reaches every fresh profile. Pinning the default set to a version and a publisher in the app's config bounds that.
- Does the operator key survive at all? Keeping it as an accepted signer for the default set is a hedge against the name-owner key being lost; keeping it forever means the two-rules problem never fully goes away.