Skip to content

Plugin SDK

Verified against v0.21.0 · sdk/plugin-sdk/src/host.js, sdk/plugin-sdk/src/cli.js, sdk/plugin-sdk/src/bundle.js, sdk/plugin-sdk/src/settings.js, sdk/plugin-ui/parley.css, internal/api/plugindev.go, internal/api/plugins.go, internal/plugin/describe.go

@parley/plugin-sdk is the guest library: capability-aware methods that fail fast in the guest when a grant is missing, then still call the Extism bridge, which remains the enforcing check. @parley/plugin-ui is optional CSS: add a class (parley-btn, parley-panel, parley-input) and the rule uses the host color tokens (--color-accent, --color-surface, --color-ink).

Terminal window
node sdk/plugin-sdk/src/cli.js scaffold ./my-plugin
node sdk/plugin-sdk/src/cli.js verify ./my-plugin
node sdk/plugin-sdk/src/cli.js keygen ./release # release.key (0600) and release.pub
node sdk/plugin-sdk/src/cli.js pack ./my-plugin --key ./release.key
node sdk/plugin-sdk/src/cli.js sign bundle.parley --key ./release.key
node sdk/plugin-sdk/src/cli.js types ./my-plugin # TypeScript for the settings schema
node sdk/plugin-sdk/src/cli.js dev ./my-plugin

pack writes dist/<name>-<version>.parley from manifest.json, plugin.wasm, and ui.js and slots.json when present, and prints its digest and key id. sign re-signs an existing bundle in place. Both produce output byte-identical to the server’s reference packer: a CI test reproduces the golden vectors in sdk/abi/bundle-v1/ with the test key. build still copies the UI into dist/<name>-<version>.ui.js for a PLUGIN_DIR install.

dev packs, signs with ~/.config/parley/dev.key (or --key; the key is created on first use), uploads to POST /api/catalog/bundles with the parley_session cookie from PARLEY_SESSION, and prints the install link for PARLEY_ORG (default default) on BASE_URL. The instance must trust the dev key through PLUGIN_TRUSTED_KEYS, or run locally with PLUGIN_ALLOW_UNSIGNED=true and dev --unsigned. This replaces the POST /api/orgs/{org}/admin/plugins/dev-register route compiled only with -tags plugindev, which installs a bare package with no bundle and is kept for the server’s own tests. Build your first plugin walks through the whole loop.

The guest wrapper generateGuestHookTypes prints TypeScript signatures for on_session_state, on_session_action, on_event and on_job from sdk/abi/v1.json.

The wire protocol is frozen at version 1; see Plugin wire protocol. The Plugin API reference documents every hook input, host function request and response, and the window.parley UI bridge.

createHost(grants, call, manifest) adds getSettings(), which calls parley_settings_get and returns the non-secret values, and getSecret(name), which reads a secret settings field through parley_secret_get. getSettings() throws in the guest, without calling the host, if the manifest declares no settings. A "format": "secret" field is an implicit secrets grant for its own name, so getSecret allows it without a separate grant. generateSettingsTypes(manifest) prints TypeScript: a Settings interface of the non-secret fields (an enum becomes a union, a field absent from required without a default is optional, since the host applies defaults) and a SecretName union of the secret ones.

A plugin manifest (manifest.json; a legacy package.json is still read) is JSON with manifest: 1, kind: "plugin", name, version (major.minor.patch), optional quotaBytes, capabilities (each capability plus optional scope), kinds for ceremonies the plugin provides, and optional slots for UI chrome: panel, room, toolbar, nav, and export-menu, and an optional settings schema (see Settings above). An unknown slot is refused at preview and install. A plugin that does not declare a slot appears nowhere in that chrome; a UI bundle with no sidecar still occupies the nested panel so existing installs keep working. parley-plugin build writes <name>-<version>.slots.json beside the UI bundle. Installing through the operator API still requires grantsAccepted: true. Consent sentences are not written in the frontend; they come from internal/plugin/describe.go, next to the guards that enforce them.

Quoted from Describe in internal/plugin/describe.go:

  • fetch (scope is an allowlist host): “Can send anything it holds — including session data it has read — to “ plus the expanded host summary.
  • kv (unscoped): “Can store and read back data of its own on this server, under any key. It cannot see what another plugin stored.”
  • kv (scoped): “Can store and read back data of its own on this server, under keys beginning <scope>. It cannot see what another plugin stored.”
  • secrets (unscoped): “Can store and read back every secret on this instance — API tokens, passwords — encrypted on this server. Combined with any outbound access below, it can send them.”
  • log: “Can write lines into this server’s log, where they sit alongside Parley’s own.”
  • session:read (unscoped): “Can read the live state of every room running a ceremony this plugin itself provides, in any space in this org: who is seated, which of them is looking at its panel, and whatever that ceremony records. It cannot read a planning poker or standup room, or any other plugin’s rooms.”
  • session:patch (unscoped): “Can change the live state of every room running a ceremony this plugin itself provides, in any space in this org — move that room’s phase, reveal or hide what it is showing everyone in it. It cannot touch a planning poker or standup room, or any other plugin’s rooms.”
  • jobs: “Can schedule its own work to run later on this server, outside any request a person made.”
  • emit: “Can publish events of its own, which other plugins receive as though Parley had raised them.”
  • events (unscoped): “Is sent a copy of every event Parley raises, payload included.”

An unknown capability is described as: “Parley does not recognize the capability … and cannot say what it permits. Do not grant it.”