Plugin bundle format
Verified against v0.21.0 · sdk/abi/bundle-v1.md, internal/plugin/bundle/bundle.go, cmd/parley/plugincmd.go, internal/api/catalog.go, internal/plugin/pin.go, internal/api/plugins.go, internal/plugin/settings.go, internal/api/pluginsettings.go
A plugin ships as one .parley file: a deterministic gzip-compressed tar
archive. The full specification, including the byte-level encoding a packer
must reproduce, is sdk/abi/bundle-v1.md; golden vectors live beside it in
sdk/abi/bundle-v1/.
Contents
Section titled “Contents”| File | Required | Notes |
|---|---|---|
manifest.json |
yes | package.json is accepted as a legacy alias, never both |
plugin.wasm |
yes | at most 10 MiB |
ui.js |
no | the plugin UI script |
slots.json |
no | UI slot declarations |
MANIFEST.sha256 |
yes | <hex sha256> <name> per file, sorted |
MANIFEST.sig |
when signed | Ed25519 public key (32 bytes) then signature (64 bytes) |
A bundle’s identity, its digest, is the hex sha256 of MANIFEST.sha256.
Signing
Section titled “Signing”The signature is pure Ed25519 over "parley-bundle-v1\n" followed by the
MANIFEST.sha256 bytes. The signer is identified by a key id — the first
8 bytes of sha256 of the public key, in hex — which the host computes itself.
Any publisher named in the manifest is ignored.
What the host refuses
Section titled “What the host refuses”A bundle is refused, before any of it is used, if it contains a file
MANIFEST.sha256 does not list, lacks a file it does list, or has a file whose
digest does not match; if an entry name has path components, is a symlink or
other non-regular entry, is duplicated, or is not one of the names above; if
plugin.wasm exceeds 10 MiB or the uncompressed archive exceeds 16 MiB
(tar headers and padding included, counted while decompressing), or the
entries’ declared sizes add up past it; if anything follows the gzip member; if
MANIFEST.sha256 is not in canonical form (sorted, lowercase hex, two spaces,
a newline after every line); or if its signature does not verify, is by an
untrusted key, or is missing and unsigned bundles are not allowed.
The catalog
Section titled “The catalog”Verified bundles are stored in the database, so every replica serves the same
bytes. An admin of the default org uploads a bundle on the Plugin
catalog page (/catalog), one or several files at a time, each reported on
its own, or with POST /api/catalog/bundles (one bundle per request); nobody
else may, including an admin of another org. Every org member can browse the
catalog: each plugin shows once what its latest version would be permitted to
do, and each version row its signer, digest, publish date and what it adds or
drops compared with the version before it.
A bundle is accepted only if it is signed by a key in PLUGIN_TRUSTED_KEYS,
or is unsigned and PLUGIN_ALLOW_UNSIGNED=true. The first bundle stored for a
name and version holds it: a different bundle for the same name and version is
refused, so a new release needs a new version. Stored bundles are never
deleted.
Installing by digest
Section titled “Installing by digest”An org admin picks a version on their org’s Plugins page, or follows the
Install in <org> link beside it on the catalog page, and grants what the
server says it may do before anything is installed.
An org admin installs a catalog version by its digest and key id, and the
install is pinned to that row: the host loads it by digest, never by name
and version, so the install runs exactly those bytes, and trust is still
re-checked on every call. The grants, session kinds and consent copy come from
the verified bundle’s own manifest. A catalog bundle is the only way to
install or upgrade; there is no upload of a bare package.json.
At boot, after the PLUGIN_DIR import, every install with no pin, or a pin that no longer matches its version, is pinned to the
trusted stored bundle for its name and version (signed preferred), under its own advisory lock.
An install with no match is never disabled: it keeps running from loose files
and the plugins page marks it not in catalog.
An install can move to any trusted catalog version of the same plugin, older or newer. Going to an older version is a rollback, confirmed in two steps and audited with the version and bundle it moved from and to; it is the only way back, since installing an older version directly is refused. A move that asks for wider grants than the ones in force waits for approval like any widening upgrade. Every bundle an install is pinned to is remembered and shown as its history. Stored bundles are never deleted; retention and garbage collection of unreferenced bundles are not implemented yet.
GET /api/catalog/loaded, for default-org admins, names the bundles the
answering replica has compiled, by digest and key id, to confirm every replica
runs the same bytes.
A pin is authoritative only while its bundle carries the install’s version. An
older binary during a rolling upgrade can change an install’s version without
moving its pin. That install then runs only the trusted stored bundle for its
new version, and is re-pinned to it; with none stored it fails closed. An
install that has ever been pinned never runs a loose PLUGIN_DIR file. Avoid plugin admin actions while a rollout is in progress.
plugin_installs and plugin_install_history hold foreign keys to
plugin_bundles, so any future garbage collection has to leave every bundle an
install is pinned to, staged for, or ran (or clear that history first).
Settings
Section titled “Settings”A manifest may declare settings: a flat JSON Schema subset an org admin fills
in on the install’s card. The host validates it with its own code, and a
schema outside the subset is refused at catalog upload (400) and at install.
{ "type": "object", "properties": { "channel": { "type": "string", "title": "Channel", "pattern": "^#[a-z]+$", "default": "#general" }, "size": { "type": "integer", "minimum": 1, "maximum": 10 }, "mode": { "type": "string", "enum": ["fast", "slow"] }, "token": { "type": "string", "title": "API token", "format": "secret" } }, "required": ["mode"], "additionalProperties": false}typeisobject;additionalPropertiesisfalseor left out. Nothing nests: each property isstring,number,integerorboolean.- A property may carry
title,description,default,enum,pattern(strings, Go RE2 syntax),minLength/maxLength(strings, in characters),minimum/maximum(numbers), andformat: "secret"(strings). Any other keyword is refused, so the host never ignores a rule the admin was shown. - At most 32 properties, each named with lowercase letters, digits and underscores, starting with a letter, at most 40 characters.
The plugin reads the non-secret values, with defaults applied, through
parley_settings_get. It needs no grant, it is refused while the install is
disabled, and it re-reads the database on every call.
Secrets. A format: "secret" field is never stored with the other values.
It is an implicit secrets grant scoped to the field’s name, so it appears on
the consent screen, and a version that adds one is a widening upgrade that waits
for approval. It is stored encrypted like any plugin secret and read only
through parley_secret_get with the field name. The admin API never returns
its value: only whether it is set, and whether a configured key can open it.
Upgrades and rollbacks. Moving an install to another version re-checks the saved values against that version’s schema, at the request and again at approval. If any saved value does not fit, the move is refused with 409 naming the fields; change or clear them, then try again. Nothing is dropped silently. A field the new version newly requires is not a refusal, since it could not be filled in before; it is enforced at the next save.
A saved value for a field the target version does not declare counts as not
fitting, and that includes moving to a version with no settings at all: the
move is refused until the admin clears those fields. An approval re-checks
only when the pending upgrade names a stored bundle. An upgrade staged without
one, from before the catalog, has no schema to check against, and its approval
skips the check.
Operator commands
Section titled “Operator commands”parley plugin keygen [path-prefix] # writes <prefix>.key (0600) and <prefix>.pubparley plugin verify -key <base64-public-key> bundle.parleykeygen refuses to overwrite either file and prints the key id and the base64
public key to pass to verify -key. verify accepts -key
more than once and -allow-unsigned; it prints the digest and key id, or the
reason for refusal, and exits non-zero on refusal. Packing and signing are done
by the plugin SDK, not the server binary.