8.0 KiB
Plugin Manifest Reference
The plugin manifest is the contract between an installable WASM binary and the host runtime. The host parses the manifest with the Zod schema in
packages/plugin-runtime/src/manifest.tsand rejects anything that does not validate — adding a field here without updating the schema does nothing.
The manifest may be TOML or JSON (whichever your toolchain emits). The
runtime works against the parsed object, not the source bytes — except
during signing, where the canonical form is the sorted-keys JSON
projection (see cli-reference.md#sign).
Top-level fields
| Field | Type | Required | Notes |
|---|---|---|---|
schemaVersion |
1 |
yes | Bumped on breaking schema changes. Host rejects unknown versions. |
id |
reverse-DNS string | yes | Pattern ^[a-z]+(\.[a-z0-9-]+){1,}$. Globally unique across hosts. |
version |
semver | yes | MAJOR.MINOR.PATCH[-prerelease]. Host enforces SemVer for upgrades. |
name |
string ≤ 256 chars | yes | Display name. |
description |
string ≤ 2048 chars | no | Free-form for the marketplace. |
entry |
string ≤ 256 chars | yes | Path of the wasm relative to the manifest, e.g. plugin.wasm. |
wasmSha256 |
64-char hex | yes | SHA-256 of the binary at signing time. Re-checked on every load. |
signature |
128-char hex | yes | Ed25519 signature over canonicalManifest:wasmSha256. |
signedBy |
64-char hex | yes | Raw 32-byte ed25519 public key (hex). Registered with operator. |
allowedHosts |
array of public hostnames | no | Outbound HTTP allowlist for http.outbound. Empty disables HTTP. No scheme, port, localhost, IP literal, .local, or .internal. |
capabilities |
array of capability entries | no | Capability declarations. See below. |
hooks |
array of hook entries (≥1) | yes | At least one hook must be declared; a zero-hook plugin never runs. |
ui.settings.entry |
relative bundle path | no | Optional settings HTML entry rendered by the host in a sandboxed PluginSettingsFrame. Absolute paths, URL schemes, and .. segments are rejected. When present, the settings entry must also declare ui.settings.sha256 or ui.settings.integrity so the host can verify the bundled HTML before rendering it. |
Settings HTML communicates with the host through the versioned
plugin.settings.* postMessage protocol documented in
sdk-reference.md#settings-iframe-protocol.
Capability entries
[[capabilities]]
name = "http.outbound"
scope = { allowedHosts = ["hooks.slack.com"] }
[[capabilities]]
name = "secret.read"
scope = { keyPattern = "slack-webhook-.*" }
Each entry has:
-
name— capability name from the closed enum (seecapability-matrix.md). -
scope— optional, capability-specific narrowing. Permitted shape:Field Allowed for Meaning allowedHostshttp.outboundHostname allowlist (no scheme, no port) keyPatternsecret.readRegex (1-128 chars) the key must match tablePrefixdb.read,db.writeRequired prefix; pattern ^[a-z][a-z0-9_]*_$Other keys are rejected by
.strict().
Hook entries
[[hooks]]
name = "entity.created"
export = "handleEntityCreated"
name— host hook event name; must match an entry the host emits.export— function name exported from the wasm binary that handles the hook.
Both fields cap at 128 characters.
Foundation Hook Payloads
The SDK exports matching payload types from @toolctrl/plugin-sdk
(TypedHookHandler<"entity.updated">, PluginHookWorkflowExecutedPayload,
and friends). Runtime validation is strict, so handlers should rely on
these field names instead of ad-hoc shapes.
| Hook | Payload summary |
|---|---|
tenant.created |
tenantId, slug, optional ownerEmail, optional plan, createdAt |
user.created |
userId, optional nullable tenantId, email, optional locale, createdAt |
entity.created / entity.updated / entity.deleted |
entityType, entityId, nullable tenantId, optional userId, optional changedFields, occurredAt |
workflow.created / workflow.updated / workflow.deleted |
nullable tenantId, workflowId, nullable userId, status, appliesToAllTenants, optional changedFields, occurredAt; workflow blocks, credentials, and raw input are intentionally not included. |
workflow.executed |
tenantId, workflowId, workflowExecutionId, optional nullable userId, type, status, durationMs, errorCode (null for success, required for error), appliesToAllTenants, occurredAt; raw execution input, output, and error bodies are intentionally not included. |
workflow.failed |
tenantId, workflowId, workflowExecutionId, optional nullable userId, type, fixed status: "error", required errorCode, durationMs, appliesToAllTenants, occurredAt; raw execution input, output, and error bodies are intentionally not included. |
Strictness
The schema uses Zod's .strict() everywhere — unknown fields cause
parse failure. This is intentional: extending the manifest is a
deliberate, host-side operation, never a plugin-author hack.
Canonical bytes (for signing)
The CLI and runtime verifier both sign and verify
<canonicalManifestJSON>:<wasmSha256> where the canonical form is the manifest
minus signature, signedBy, and wasmSha256, projected to JSON with deeply
sorted keys (see canonicalPluginSignaturePayload in
packages/plugin-runtime/src/signature.ts).
This means adding / removing capabilities, hooks, or any other field invalidates the signature — re-signing is required for every authoring change.
Example (TOML)
schemaVersion = 1
id = "de.acme.slack-notifier"
version = "1.2.0"
name = "Slack Notifier"
description = "Sends tenant events to a Slack channel."
entry = "plugin.wasm"
wasmSha256 = "a1b2..."
signature = "c3d4..."
signedBy = "e5f6..."
allowedHosts = ["hooks.slack.com"]
[[capabilities]]
name = "log"
[[capabilities]]
name = "secret.read"
scope = { keyPattern = "slack-webhook-.*" }
[[capabilities]]
name = "http.outbound"
scope = { allowedHosts = ["hooks.slack.com"] }
[[hooks]]
name = "entity.created"
export = "handleEntityCreated"
[[hooks]]
name = "entity.updated"
export = "handleEntityUpdated"
Versioning + compatibility
Plugins may carry a minHostVersion / maxHostVersion constraint
(see versioning.md). Install rejects manifests whose declared host-version
range does not include the current host version.