WrengleWrengle
Manifest, actions, and capabilities

Manifest, actions, and capabilities

Beta

This page documents the local WASM action plugin metadata Wrengle currently validates: [runtime], [[actions]], capabilities, and host functions.

If your plugin needs custom UI instead of a WASM action, use Views (custom plugin UI). View-only plugins do not need plugin.wasm.

Folder layout

text
my-plugin/
  plugin.toml
  plugin.wasm

plugin.wasm must be built or copied next to plugin.toml before the plugin can load.

Generated scaffold layout

Settings -> Plugins -> Create plugin creates a standalone WASM action plugin project:

text
my-plugin/
  README.md
  Cargo.toml
  plugin.toml
  src/
    lib.rs

Build the scaffold from the Wrengle repository root, not from inside the plugin folder:

sh
just plugin-build "<plugin_dir>"
just plugin-smoke "<plugin_dir>"

The build command writes plugin.wasm next to plugin.toml. Reload the local path after that file exists. Generated Rust action scaffolds use wrengle-plugin-sdk from this repository through a local Cargo path dependency; if the generated folder moves, update that dependency to point at crates/plugin-sdk in your Wrengle checkout.

Manifest example

toml
id = "plugin.example.echo"
name = "Echo"
version = "0.1.0"
schema_version = 1
description = "Echoes selected text."
 
[runtime]
kind = "wasm"
entry = "plugin.wasm"
 
[[actions]]
id = "plugin.example.echo.run"
label = "Echo selection"
short_scheme = "echo"
description = "Echo selected text."

Validation rules

Wrengle rejects the whole plugin for plugin-level manifest failures: invalid plugin id, unsupported schema_version or runtime.kind, missing or invalid runtime.entry, no declared actions for a WASM action plugin, duplicate action ids, malformed TOML, or manifest size/shape limit violations.

Action-level validation failures are recorded as diagnostics and skip only the affected action. Valid actions must have ids that start with the plugin id plus a dot, and short_scheme must start with a lowercase ASCII letter followed by only lowercase ASCII letters, digits, and hyphens.

label appears in the editor Actions menu. short_scheme is used for action:// links when an action returns a reference.

Action request

Wrengle calls the WASM export run_action with this JSON envelope:

json
{
  "actionId": "plugin.example.echo.run",
  "request": {
    "selection": "selected text",
    "context": {},
    "config": {}
  }
}

Minimal action export

A minimal action export can be:

rust
use extism_pdk::{plugin_fn, FnResult, Json};
use serde_json::json;
use wrengle_plugin_sdk::{ActionResult, RunEnvelope};
 
#[plugin_fn]
pub fn run_action(Json(envelope): Json<RunEnvelope>) -> FnResult<Json<ActionResult>> {
    let RunEnvelope { action_id, request } = envelope;
    let selection_length = request.selection.chars().count();
 
    Ok(Json(ActionResult {
        summary: format!("Echoed {selection_length} characters"),
        reference: None,
        data: json!({
            "actionId": action_id,
            "selection": request.selection,
            "selectionLength": selection_length,
        }),
    }))
}

Action result

json
{
  "summary": "Echoed selected text",
  "reference": null,
  "data": {}
}

If reference is non-null, Wrengle can create an inline action:// link using the action's short_scheme. If reference is null, Wrengle still shows the summary and records the action, but no link is inserted into the note.

Capability examples

Plugins are denied host access by default. Declare the host access the plugin needs:

toml
[[capabilities]]
scope = "httpHosts"
hosts = ["api.example.com"]
 
[[capabilities]]
scope = "secret"
keys = ["example.token"]
 
[[capabilities]]
scope = "readVault"
 
[[capabilities]]
scope = "noteRead"
 
[[capabilities]]
scope = "noteWrite"
 
[[capabilities]]
scope = "blockRead"
 
[[capabilities]]
scope = "blockWrite"
 
[[capabilities]]
scope = "dashboardRead"
 
[[capabilities]]
scope = "pluginState"

Declarations are not grants. Capability declarations describe the host APIs a plugin may ask for; they do not grant host access by themselves. Grants are local to this app install and are keyed to the plugin id, canonical local path identity, and currently declared grant keys.

Grant keys shown in Settings include exact parameterized keys such as httpHosts:api.example.com and secret:github.token, plus unparameterized keys such as noteRead, noteWrite, blockRead, blockWrite, dashboardRead, and pluginState. These grants gate host functions. A missing grant returns a host error instead of giving the plugin direct filesystem, process, network, or keychain access.

dashboardRead is reserved for future workspace-aware plugin surfaces. Current Plugin Builder output is limited to static custom views and offline feature apps with plugin-scoped JSON state, so Builder-created apps do not receive dashboardRead, dashboard.snapshot, note or block reads, note or block write proposals, network access, secrets, shell access, or generated WASM actions. Manual local plugin views may declare the scope, but cannot use dashboard.snapshot yet.

Plugin state and secrets

Secret aliases must be lowercase ASCII letters, digits, ., -, or _. Wrengle stores plugin secret values in the OS keychain under a namespace scoped to the plugin id and the canonical plugin path identity. If the same plugin id is loaded from a different canonical path, it cannot read the old path's secret values. Startup preloads only aliases for the current installed identity that already have a secret grant; the renderer and plugin receive no raw inventory or physical Keychain name. Successful values stay in the zeroizing native session cache, while an unavailable item leaves secret-dependent host calls disabled for that session. Runtime secret changes fail closed rather than showing a Keychain prompt during plugin use.

pluginState grants access to plugin-scoped JSON state through the host API. State is isolated by plugin id and canonical path identity. Keys may be up to 256 bytes, keys starting with wrengle. are reserved, each JSON value is capped at 32 KiB, and one plugin subject is capped at 1 MiB.

Builder-created offline apps receive plugin-scoped JSON state automatically. Wrengle keeps that local app data with the installed Builder app until the user resets app state or deletes the app. Builder drafts declare expected state keys before Apply, and installed Builder app calls are limited to those declared state keys. Manual local plugins use the same isolated plugin state host API when their manifest requests it and the user grants it. Plugin state is local to this device and is not a sync or backup boundary.

Terminal access

There is no plugin terminal capability. Local WASM plugins cannot create terminal tabs, write shell input, read terminal output, or receive selected terminal output. Manifest scopes such as terminal are invalid and the plugin will not load.

External ACP agents can request terminal commands only through visible proposal cards that the user must run explicitly. Terminal output can reach an agent only when the user selects output and attaches the bounded snippet.

Host functions

The plugin runtime exposes these host imports:

Host functionCapabilityInputOutput
wrengle_httphttpHosts for the exact destination hostHttpRequest JSON: { "method": "POST", "url": "https://api.example.com/path", "headers": [], "body": "{}" }HostCallEnvelope<HttpResponse> JSON
wrengle_secretsecret with the requested keySecret key stringHostCallEnvelope<string> JSON
wrengle_read_notereadVault or noteReadVault-relative Markdown note path stringHostCallEnvelope<string> JSON
wrengle_write_notenoteWriteNoteWriteRequest JSON with a Markdown note pathHostCallEnvelope<NoteWriteResult> JSON with status: "pendingUserApproval", a proposal id, and a line diff
wrengle_read_blocksblockReadBlockReadRequest JSON with a Markdown note pathHostCallEnvelope<BlockReadResult> JSON with the current block snapshot, content hash, and optional revision
wrengle_write_blocksblockWriteBlockWriteRequest JSON with a Markdown note pathHostCallEnvelope<BlockWriteResult> JSON with status: "pendingUserApproval", the queued block ops, and preview blocks
wrengle_plugin_state_getpluginStateState key stringHostCallEnvelope<PluginStateGetResult> JSON, for example { "found": false, "value": null } when missing
wrengle_plugin_state_setpluginStatePluginStateSetRequest JSON: { "key": "count", "value": 1 }HostCallEnvelope<null> JSON
wrengle_plugin_state_deletepluginStateState key stringHostCallEnvelope<null> JSON

Note path inputs for wrengle_read_note, wrengle_write_note, wrengle_read_blocks, and wrengle_write_blocks must be vault-relative Markdown files under notes/, projects/, or people/. Paths with absolute prefixes, whitespace padding, ./.. segments, hidden path segments, backslashes, empty segments, non-.md suffixes, or other roots are rejected before the host reads or writes the vault.

Write host functions never apply changes directly from plugin code. They create app-mediated proposals visible in the action dialog. Denying a proposal marks it denied. Approving a note proposal re-checks the current note hash and any expected revision guard against the proposal base; if either check no longer matches, Wrengle marks the proposal conflict and does not write. Approved note proposals use the same vault save and commit path as normal note saves.

NoteWriteRequest.newMarkdown is the complete raw note file content, matching wrengle_read_note output. Include YAML frontmatter in newMarkdown when the approved replacement should have frontmatter; omit it when the replacement should have none.

Plugin noteWrite proposals are distinct from ACP agent edit approvals: Wrengle previews and conflict-checks the plugin-supplied full Markdown replacement, but it does not reinterpret that payload as structured block ops. For list, checklist, table, or other block-precise changes, prefer wrengle_write_blocks so the proposal is expressed structurally. Wrengle preserves the current note sidecar metadata during note proposal approval until plugins have a structured sidecar/block write contract.

Block write approval re-reads the current note, checks the proposal hash and any expected revision, then applies the approved block edit through the same vault save and commit path as note proposals. Conflicts are reported instead of silently overwriting current note content. Block proposals currently support insert, update, and remove operations. Move operations return a structured plugin error until native block reordering is wired into the shared block editor.

Each host-function result is envelope JSON. Success is shaped like { "status": "ok", "value": ... }; errors are shaped like { "status": "err", "error": { "kind": "capabilityDenied", "message": "..." } }. The Rust SDK maps those envelopes to typed Result values such as Error::CapabilityDenied, Error::UserDenied, Error::InvalidRequest, and Error::Conflict.

HTTP redirects are not followed, request hosts must exactly match the allow-list, HTTP methods are limited to POST, PUT, and PATCH, and the plugin runtime enforces a bounded execution window and memory budget.

wrengle_http also requires HTTPS URLs with no credentials or fragments, rejects localhost and private or special IP targets after DNS resolution, caps request bodies at 64 KiB and response bodies at 32 KiB, and applies 15 s total / 5 s connect HTTP timeouts.

Load and run checklist

  1. Build or copy plugin.wasm next to plugin.toml.
  2. Open Settings -> Plugins.
  3. Add the plugin folder as a developer plugin path. Prefer an absolute path.
  4. Click Reload.
  5. Confirm the action appears in Loaded plugins.
  6. Open a note, select text, and choose the action from the editor toolbar's Actions menu.
  7. Click Confirm in the preview dialog.

For the repository echo example:

sh
rustup target add wasm32-unknown-unknown
just plugin-echo-build
just plugin-echo-smoke

For generated Settings scaffolds, use the generic commands with the generated folder path:

sh
just plugin-build "<plugin_dir>"
just plugin-smoke "<plugin_dir>"

Troubleshooting

SymptomWhat to check
missing plugin.wasm or missing runtime.entry fileRun just plugin-build "<plugin_dir>", then reload the local path.
Short-scheme collisionChange short_scheme in plugin.toml; every registered action needs a unique scheme.
Runtime failureRun just plugin-smoke "<plugin_dir>" and inspect the action error reported by the host.
Action not appearingConfirm the folder is listed in Settings -> Plugins, click Reload, then select text again to reopen Actions.
docs / plugins/manifest-actions-capabilitiesAll documentation