Files
HiveGrid-Wireframes/hivegrid-cloud/docs/plugins/manifest-reference.md
T

149 lines
8.0 KiB
Markdown

# 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.ts` and 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`](sdk-reference.md#settings-iframe-protocol).
## Capability entries
```toml
[[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 (see
`capability-matrix.md`).
- `scope` — optional, capability-specific narrowing. Permitted shape:
| Field | Allowed for | Meaning |
| -------------- | --------------------- | --------------------------------------------- |
| `allowedHosts` | `http.outbound` | Hostname allowlist (no scheme, no port) |
| `keyPattern` | `secret.read` | Regex (1-128 chars) the key must match |
| `tablePrefix` | `db.read`, `db.write` | Required prefix; pattern `^[a-z][a-z0-9_]*_$` |
Other keys are rejected by `.strict()`.
## Hook entries
```toml
[[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)
```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.