Skip to content

Plugins: operate

Verified against v0.21.0 · cmd/parley/main.go, cmd/parley/plugincmd.go, internal/api/catalog.go, internal/api/plugins.go, internal/api/router.go, internal/plugin/pin.go, internal/plugin/bundle/bundle.go, web/src/pages/PluginsPage.tsx, web/src/pages/CatalogPage.tsx, sdk/abi/bundle-v1.md

This is the operator’s side of the plugin system: who is trusted, where bundles live, who may install them, and what to do when a key changes. The bundle format is in Plugin bundle format; what a plugin may do is in Plugin sandbox; writing one is Build your first plugin.

A plugin is a signed .parley bundle. Bundles are stored in Postgres, in an instance-wide catalog, and every replica re-verifies a stored bundle against its own trust set before using it. An org admin installs a catalog version into their org; the install is pinned to that bundle’s digest, so it runs exactly those bytes. Nothing is fetched at run time.

The org’s Plugins page describes only that org’s installs and grants. If the catalog or installed-plugin list cannot be loaded, Try again retries the request without changing any install or capability grant.

Parley runs only bundles signed by a key you list in PLUGIN_TRUSTED_KEYS (comma-separated base64 Ed25519 public keys; unset trusts none). A malformed entry stops the boot.

  1. The publisher generates a pair with parley plugin keygen, which prints the key id and the public key, and sends you the public key (the .pub file).
  2. Get the key id out of band — from the publisher’s release notes, a signed message, a call — and compare it with the id you compute yourself. Do not take both from the same message you received the key in. The key id is the first 8 bytes of sha256 of the public key, in hex; keygen prints it, and parley plugin verify prints the id of whoever signed a bundle.
  3. Add the public key to PLUGIN_TRUSTED_KEYS and restart every replica. The trust set is read at boot.
  4. Check a bundle before you upload it: parley plugin verify -key BASE64_PUBLIC_KEY bundle.parley prints the digest and key id or the reason for refusal.

The Parley project’s own key id will be published in SECURITY.md; it is not in this page, because a page you fetched from the same place as the bundle proves nothing.

Removing a key revokes it. Take it out of PLUGIN_TRUSTED_KEYS and restart each replica; trust is checked on every call, so the bundle stops running on that replica’s next call after it restarts. PLUGIN_ALLOW_UNSIGNED=true also accepts unsigned bundles. Leave it off unless you build every bundle yourself.

An admin of the default org uploads on the Plugin catalog page (/catalog), several files at a time if wanted, or with POST /api/catalog/bundles (one bundle per request). Admins of other orgs cannot upload. Every org member can browse the catalog, and each version shows its signer, digest, publish date, and what it adds or drops against the previous version.

A bundle is stored only if its key is trusted. The first bundle stored for a name and version holds it: a different bundle for the same name and version is refused, so a fix ships as a new version. Stored bundles are never deleted (see known limits).

An org admin opens Plugins under the org’s admin area (/o/ORG/admin/plugins, or Install in beside a version on /catalog) and picks a version. The install is per org: enabling it in one org changes nothing in another.

  • Consent. The screen lists what the version may do, in wording generated by the host. Installing needs an explicit acknowledgement; the server refuses to act without one.
  • Approvals. An upgrade that asks for wider grants than the ones in force waits. The old version keeps running on the old grants until an admin approves the new ones; an upgrade that asks for nothing new applies directly.
  • Disable and uninstall. Disabling is reversible. Uninstall deletes the plugin’s stored data and secrets and is refused while any session of a kind it provides still exists.

Every install, upgrade, approval, rollback, enable, disable and uninstall writes a row to org_audit_log.

A plugin may declare settings. They appear on the install’s card and are validated by the host; the schema subset is in Plugin bundle format. A field marked format: "secret" is write-only: it is shown on the consent screen as a secrets grant, stored encrypted, and never returned by the admin API, which reports only whether it is set and whether a configured key can open it. Secrets need PLUGIN_SECRET_KEY; without it a plugin asking for one fails to install.

An install can move to any trusted catalog version of the same plugin, chosen from the install chooser (the Plugins page, or an Install link on /catalog). The install’s own card offers only the versions it has already run. Moving to an older one is a rollback: it takes two clicks and is audited with the version and bundle it moved from and to. Installing an older version directly is refused. Saved settings must fit the target version’s schema, or the move is refused with 409 naming the fields.

A name and version holds one signed bundle. An unsigned bundle is stored only when nothing holds that name and version, but a signed one is still accepted after it, so one version can have both. Moving an install to another bundle of the same version re-pins it. The one case that matters: moving a running signed install onto the unsigned bundle of the same version, which is possible only with PLUGIN_ALLOW_UNSIGNED=true. It lowers the trust the install runs under and today has no extra warning (#737). Leave PLUGIN_ALLOW_UNSIGNED off and that move is not available.

Removing a kind in an upgrade or rollback leaves its existing rooms in place but degraded; the room shows the “kind unavailable” state.

The catalog, the installs and their pins are all in Postgres, so a change made through any replica applies on every one without a restart: a running host re-checks which bundle answers at most every 5 seconds. Two things are per-replica: the trust set and the secret keys, both read at boot, so change them with a rolling restart of every replica.

Avoid plugin admin actions during a rollout. An older binary can change an install’s version without moving its pin. The newer binary then runs only the trusted stored bundle for the new version and re-pins to it, and with none stored it fails closed.

Plugin secrets and standup webhook secrets are encrypted under PLUGIN_SECRET_KEY. To rotate without losing them:

  1. Roll out PLUGIN_SECRET_KEY = the old key and PLUGIN_SECRET_KEY_PREVIOUS = the new key to every replica.
  2. Roll out PLUGIN_SECRET_KEY = the new key and PLUGIN_SECRET_KEY_PREVIOUS = the old key to every replica.
  3. Run parley secrets reseal once, with the same environment (a one-off Job or kubectl exec on Kubernetes).
  4. Remove PLUGIN_SECRET_KEY_PREVIOUS only after a run that exits 0 and logs secrets reseal finished with remaining 0.

Rotating without the previous key makes every stored secret undecryptable. The full behavior, including the bound and unbound forms, is under PLUGIN_SECRET_KEY_PREVIOUS.

  • GET /api/catalog/loaded, for default-org admins, lists the bundles the answering replica has compiled, by digest and key id. Call it once per replica to confirm they all run the same bytes.
  • A plugin card shows its health and last error, and the bundle digest it is pinned to.
  • Plugin admin actions are in org_audit_log. A loose-file read logs a warning, at most once every few seconds per plugin version, which is the signal that something has not been moved to the catalog.
  • At boot Parley logs how many secrets are not yet under the current key.

Moving from loose PLUGIN_DIR files to the catalog

Section titled “Moving from loose PLUGIN_DIR files to the catalog”

Before the catalog, a plugin was <name>-<version>.wasm (and .ui.js, .slots.json) in PLUGIN_DIR, installed from a package.json. Those files are legacy and unsigned. They keep working, and moving off them is a few steps:

  1. Get a signed bundle of the same plugin and version, from the publisher, or pack and sign one with the plugin SDK.
  2. Trust the signer. Add the public key to PLUGIN_TRUSTED_KEYS as above and restart every replica.
  3. Put the bundle in the catalog. Either upload it on /catalog, or drop the .parley file into PLUGIN_DIR: at boot every *.parley file there is verified and stored, and one that is refused is logged and skipped.
  4. Restart once. After the import, every install with no pin, or a pin that no longer matches the install’s version, is pinned to the trusted stored bundle for its own name and version (a signed bundle is preferred over an unsigned one), under its own advisory lock so replicas booting together agree. The install is not disabled or changed; it now runs the bundle by digest.
  5. Check the plugins page. An install with no matching bundle is never disabled: it keeps running from the loose files and shows Not in catalog — still runs from PLUGIN_DIR. Upload the matching bundle, or move the install to a catalog version from its card.
  6. Remove the loose files once no install shows that status. An install that has ever been pinned never runs a loose file again, and a loose file is read only when no bundle at all is stored for that name and version.

A catalog bundle is the only way to install or upgrade from now on; there is no upload of a bare package.json. Keeping PLUGIN_DIR set is harmless: it is only a place to import .parley files from. It is no longer needed to run the plugin host, which runs either way.

  • No garbage collection of stored bundles. Bundles are append-only, and the database holds every one you have ever uploaded. Retention and a way to delete an unreferenced one are not built (#737).
  • A trust-lowering re-pin has no warning, as above.
  • Revoking a key needs a restart of every replica.
  • The catalog has one curator group, the admins of the default org. A marketplace with several publishers is a roadmap item.