Working notes and archive
Plugin implementation status
This is an engineering working note kept for audit. It records what was planned or reviewed at a point in time and is not a description of current behavior. For how Agora works today, read the task guides or the reference documentation.
What is actually built, what is prototype, and what is not there at all. Kept separate from
README.md so the author guide can describe the feature while this file stays blunt about its
limits.
API version: 0.1.1 (content providers; additive over 0.1.0). Manifest schema: 1. Both plugins_enabled and
network_plugins_enabled ship off.
Milestones
| Milestone | State | Evidence |
|---|---|---|
| P0 — script host and runtime selection | Done | crates/agora-plugin-host/tests/host.rs (21 tests) |
| P1 — host, management, recovery | Done | crates/agora-core/src/plugins/ + 63 unit tests |
| P2 — extension surfaces | Done | crates/agora-core/tests/plugins_end_to_end.rs (27 tests) against the real QuickJS host |
| P3 — first public release materials | Mostly done; see gaps below | sdk/, examples/plugins/, docs/plugins/, fixtures/ |
| P4 — author-hosted signed updates | Done, and exercised against a real host | crates/agora-plugin-api/src/distribution.rs, crates/agora-core/src/plugins/updates.rs, crates/agora/tests/signing_round_trip.rs |
| P5 — deeper customization | Closed: home replacement built, the rest declined with reasons | crates/agora-core/tests/plugins_end_to_end.rs, desktop/src/features/plugins/PluginSurface.test.tsx, BACKLOG.md |
What works
-
Install from a
.zippackage or load a development folder in place. Path traversal, decompression ratio, entry count and total size are all bounded before anything is written, and a failed extraction leaves nothing behind. -
Enable, disable, uninstall. Discarding plugin data is a separate question from removing the plugin, and a reinstall of the same id adopts the retained settings.
-
A grant is made once and compared afterwards. An update that asks for a capability — or for a network host — the installed version did not have is refused until it is accepted again, and the prompt names what changed rather than re-listing everything. An update that asks for the same or less installs without asking again, and updating a plugin the user switched off leaves it off.
-
Dependency resolution with version ranges, cycle detection, and a deterministic activation order. Unresolvability propagates up a chain rather than one level.
-
Pages, instance panels, palette and instance-context commands, declared settings, themes, diagnostics with typed repairs, and bounded pre-launch checks.
-
Per-plugin logs, event subscriptions with origin and depth tracking, and a Turn all off recovery path that does not require plugins to cooperate.
-
Activation events are honoured rather than decorative: a plugin declaring only
onView:oronCommand:is not started at launch, and one declaringonEvent:is started when that event first fires. -
A CLI surface (
agora plugin …) reaching the same core services as the GUI. -
Author-hosted updates: a plugin ships an
agora-plugin-update.jsonnaming its update URL and Ed25519 keys, which are pinned when the user agrees to install and read only from the database afterwards. Updates come from a signed, author-hosted document; the package is authenticated by the SHA-256 inside it.agora plugin keygenandagora plugin signare the author side, and a round-trip test drives the real binary and verifies the result with the real verifier, because the two halves silently breaking apart is the failure nothing else would catch. -
Automatic checking is a separate opt-in (
plugin_updates_enabled, off) from letting plugins reach the network (network_plugins_enabled, off). Neither implies the other. With it off, nothing contacts a publisher unless you press the button. -
A plugin can offer to render Agora's home screen or an instance overview. Offering is not taking: the offer joins a list in Settings and Agora's own screen renders until someone chooses otherwise. A chosen plugin that is disabled, removed or unable to run falls back to the built-in and says why, and the choice survives so re-enabling restores it.
Known limits — read before relying on any of this
This is a capability boundary, not a security sandbox. Plugins run in bounded QuickJS
runtimes on host-owned threads in the launcher's own process. That contains bugs — infinite
loops, exceptions, runaway allocation — and it does not contain malice beyond the capability
checks. A plugin you granted content:write can disable your mods, because that is what you
agreed to. The install prompt is the real control. Nothing here protects against a defect
in the engine itself.
There is no "go back to the previous version" for package bytes. During a replacement the previous package is set aside and restored if extraction or the database write fails, so a failed install never leaves you with a broken plugin. Once the new package is in place the old bytes are deleted.
Plugin data is a different story, and recoverable. When an update changes dataVersion — the
moment a plugin is about to migrate its own stored settings and possibly get it wrong — the
launcher keeps a copy first. Restore saved data in the plugin manager, or
agora plugin restore-data <id>, puts it back. Both name what they would restore and ask first.
Restoring is deliberately never automatic. Doing it on a plugin's behalf after a failed migration sounds obviously right until you notice it also discards everything the plugin wrote since the copy was taken — which, if the migration half-succeeded, is real data. The launcher cannot tell a corrupt migration from a partial one; the person using it can.
Signatures prove continuity, not identity, and there is no revocation. A verified update came
from whoever published the version you already have — nothing more. Whoever hands you the first
package chooses the key and the update URL, so a package from a bad source is bad forever and
every subsequent "update" will verify perfectly. There is no catalog, no publisher verification,
and no way to revoke a stolen key: an author whose key is taken has no channel to say so, and an
author who loses theirs cannot ship to existing installs again. docs/plugins/publishing.md says
this to authors in the same words. Replay is handled — a document carries a monotonic sequence and
an older one is refused — but that is a narrower guarantee than it sounds.
The update path has now run against a real host, and doing it found a bug no test could.
Every real call panicked — Cannot drop a runtime in a context where blocking is not allowed —
because both fetches used the blocking HTTP helper from inside an async caller. The format, the
verifier, the trust store and the install path were all covered by tests, and a CLI-signed
document provably verified in the launcher; none of that exercised a socket, because the network
gate rejects loopback by design and a local server would only prove the gate refusing it.
Fixed, and the full cycle is now demonstrated: publish, install, check, update, and a tampered package refused because it did not match the hash in the signed document. Treat this as one verified run rather than a guarantee — it has been done once, by hand, on Windows.
Custom views were withdrawn, not deferred. The data:-iframe prototype is gone: renderer,
bridge, read command, example and manifest support. A manifest declaring one is refused by name
with a migration message, pinned by a compatibility fixture.
The decisive reason was not the accessibility cost, which was known. It was that the script in that frame ran in the WebView, outside every bound the plugin runtime exists to impose — no memory ceiling, no interrupt handler, no deadline — so a 512 KiB document could hang the launcher with a loop while the QuickJS budget it was supposedly subject to looked on. Two further gaps: the frame's isolation could not be demonstrated for the packaged app (Tauri documents that on some platforms IPC from an embedded frame is indistinguishable from IPC from its window), and accessibility and controller behaviour inside the frame were the author's problem in an app where controller support is first-class.
It was removed rather than repaired because repairing it buys a feature with no demonstrated user and a permanent compatibility obligation. API 0.1 is experimental, unreleased and off by default, so withdrawing now cost one example; withdrawing later would have been a breaking change. The design history is in the commit that removed it. Reconsider only when a specific plugin needs an interaction a reasonable host-rendered component cannot provide.
The plugin host builds and passes on all three platforms; the packaged app is still unproven.
CI runs cargo test -p agora-core --lib --tests on Windows, Linux and macOS, and agora-plugin-host
is a dev-dependency of core — so all 58 plugin end-to-end tests run against the real QuickJS host on
each. They pass. rquickjs needs no external toolchain anywhere.
What that does not cover is the packaged desktop bundle with plugins enabled, on any platform
including Windows. The browser end-to-end suite mocks the Tauri bridge; the Rust suite has no
frontend. Nobody has yet launched a built .msi/.dmg/.AppImage, turned plugins on and watched a
view render.
Replacement views cover two surfaces. home and instance-overview, and nothing else. The
set is closed so that renaming an internal component is not a breaking change for plugins, and a
surface joins it only when the launcher can genuinely hand it over. Only host-rendered views may
replace a surface — a replacement is the whole
screen, and the custom-frame prototype owns its own accessibility and controller behaviour, so
standing one in for a built-in is exactly the thing "do not build a product on it" rules out.
API 0.1 is experimental. Pin >=0.1, <0.2. There is no deprecation window yet, because
there has not yet been anything to deprecate. Before v1 is advertised as stable, the items
under "Remaining before a stable v1" must be resolved.
Changing the API
The plugin API version (0.1) is not the launcher's version. They move independently: Agora can
ship 1.0 with a plugin API still at 0.2, and a plugin declares which API range it supports.
While the plugin API is 0.x:
- Breaking changes are allowed, and named in the release notes.
- A withdrawn or changed contract keeps working for one further Agora release before it is refused, and is refused by name with a message saying what replaced it — not as a parse error. The custom-view withdrawal is the worked example: the variant still deserialises purely so the refusal can explain itself.
- The
v0.1fixtures are the check. When0.2lands, every accepted fixture must still load or the break is deliberate and reviewed as such.
At 1.0 the one-release grace becomes a commitment rather than a convention, and the support
window gets a stated length. Promising one now, for an API with no published plugins, would be
theatre.
Compatibility fixtures
docs/plugins/fixtures/v0.1/ pins the shipped surface: manifests that must keep loading, and
manifests that must keep being refused for the same reason and at the same stage.
fixtures/v0.1/distribution/ does the same for agora-plugin-update.json, kept separate because
how a plugin is published and what it may do are separate agreements that change on separate
schedules. Both are enforced by crates/agora-plugin-api/tests/compatibility.rs.
When API 0.2 arrives, this directory becomes the "older supported plugin" set the plan asks for. Adding a fixture after fixing a contract bug is cheap and correct; changing one is a deliberate compatibility decision and should be reviewed as exactly that.
Remaining before a stable v1
- Verified behaviour in the packaged desktop app on macOS and Linux, not only Windows.
- Apply the deprecation policy at the first breaking change — see "Changing the API" below. The fixtures are in place; there is no v0.2 yet to run them against.
-
Honest, complete update rollback — or documentation that stops implying one exists.Plugin data can be restored from the pre-migration copy, on the user's say-so. Package bytes are still not retained, and the docs say so rather than implying otherwise. -
A real publish-and-update cycle against an author-hosted static file, not only tests.Done on 2026-09-11 againstagora-mc/governance-sandbox-testing: published a signed document and two packages, installed 1.0.0, fetched the real document over HTTPS, verified the signature against the pinned key, downloaded and installed 1.1.0. Then republished a package that did not match its signed hash and confirmed the update was refused. It found a panic on the first real call — see below. -
A revocation story, or an explicit decision that there will not be one.Decided: out of scope. There is no revocation and none is planned. With no servers there is nowhere to publish a revocation list and no authority to sign one, and building an infrastructure to carry it would cost more than the ecosystem it protects is currently worth. If a key is stolen, the author's recourse is to tell their users through whatever channel they have; the user's recourse is to uninstall.publishing.mdsays this to authors in those words. Revisit if a plugin ecosystem large enough to be worth attacking actually appears. - A curated catalog, if there is ever to be one. There is none today and installing does not need one.
-
A decision on whether the custom-view prototype becomes supported or is withdrawn.Withdrawn.
Content providers were built, reversing the earlier decision to decline them. The decline
said a content source "must never be able to certify its own artifacts, so it needs a provenance
design before an interface". That was right, and the provenance design is now written down in
providers.md and enforced in agora-core/src/providers/:
- A matching hash proves integrity, not trustworthiness. Trust is the user's grant of
content:provide, and every installed file records the provider it came from. - One rule, applied to Agora's own providers and plugins alike, and taken from Technic: a strong digest from a declared host installs quietly; weaker assurance (undeclared host, plain HTTP, MD5/SHA-1) warns and lets the user continue; no digest at all needs Allow low security downloads.
- A provider returns data only. Core downloads, verifies, snapshots and records.
Limits, bluntly:
- Modrinth and Technic are providers for Browse, detail and plans, but their install buttons
still use their older paths. The list is in
providers.md. - Provider pack installs have been exercised against the example provider's plan and the import path's own tests, not against a real third-party host.
- There is no provider catalog and no provider verification. A provider plugin is exactly as trustworthy as the person who handed you the package; see "Signatures prove continuity" above.
Install hooks and generic import/export hooks remain declined; BACKLOG.md records why.
Install observation is already served by the ten lifecycle events; install participation is a
transaction problem — ordering, veto, rollback, crash recovery — that should not be invented
speculatively.
docs/plugins/implementation-status.mdEdit this page on GitHub