WrengleWrengle
SDK and plugin creator

SDK and plugin creator

Beta

Wrengle's plugin SDK and in-app creator are for local developer-preview plugins. They do not imply marketplace availability, remote installation, or public API stability. Generated folders are trusted local code, and host access works only through grants made in this app install.

Creator templates

Open Settings → Plugins → Developer → Plugin creator to generate a local plugin folder. The creator derives a plugin.<slug> id from the name, previews the manifest, and shows the capability grants the template will request before files are created.

TemplateTypeGenerated filesCapability grants
Echo actionWASM actionCargo.toml, plugin.toml, src/lib.rs, README.mdNone by default
Note writer proposalWASM actionRust action scaffold using wrengle_plugin_sdk::hostnoteRead, noteWrite
HTTP secret actionWASM actionRust action scaffold using host::secret and host::httpsecret:<alias>, httpHosts:<host>
ViewView-only pluginplugin.toml, ui/index.html, README.mdNone by default

You can also add explicit grant keys such as pluginState, blockRead, or blockWrite before creating the scaffold. The creator writes those keys as manifest [[capabilities]] declarations; the user still grants or denies them later in Wrengle. Declarations are not grants. Plugin Builder is separate from Plugin Creator and currently creates only static custom views and offline feature apps with plugin-scoped JSON state. Builder-created apps do not receive dashboardRead, dashboard snapshots, workspace readers, workspace writers, network access, secrets, shell access, or generated WASM actions. Workspace-aware generated plugins are future work.

SDK import example

Generated WASM action templates use the repository-local Rust SDK:

toml
[dependencies]
extism-pdk = "1.4"
serde_json = "1"
wrengle-plugin-sdk = { path = "../../../crates/plugin-sdk" }

The exact relative path depends on where the generated folder lives. If you move the plugin folder, edit the wrengle-plugin-sdk path to point at crates/plugin-sdk in your Wrengle checkout.

Use SDK DTOs and host helpers from wrengle_plugin_sdk:

rust
use extism_pdk::{plugin_fn, FnResult, Json};
use serde_json::json;
use wrengle_plugin_sdk::{host, ActionResult, Error, NoteWriteRequest, RunEnvelope};
 
#[plugin_fn]
pub fn run_action(Json(envelope): Json<RunEnvelope>) -> FnResult<Json<ActionResult>> {
    let RunEnvelope { action_id, request } = envelope;
    let path = "notes/inbox/plugin-output.md";
    let existing = match host::read_note(path) {
        Ok(markdown) => markdown,
        Err(Error::NotFound(_)) => String::new(),
        Err(error) => return Err(extism_pdk::Error::msg(error.to_string())),
    };
 
    let result = host::write_note(NoteWriteRequest {
        path: path.into(),
        expected_revision: None,
        base_content_hash: None,
        new_markdown: format!("{}\n\n{}\n", existing.trim_end(), request.selection),
        summary: "Append selected text from plugin".into(),
    })
    .map_err(|error| extism_pdk::Error::msg(error.to_string()))?;
 
    Ok(Json(ActionResult {
        summary: "Prepared note write proposal".into(),
        reference: None,
        data: json!({
            "actionId": action_id,
            "proposalId": result.proposal_id,
            "status": result.status,
        }),
    }))
}

Exact grant examples

Parameterized grants include their exact scope:

toml
[[capabilities]]
scope = "httpHosts"
hosts = ["api.github.com", "api.example.com"]
 
[[capabilities]]
scope = "secret"
keys = ["github.token", "example.token"]

Settings shows those as httpHosts:api.github.com, httpHosts:api.example.com, secret:github.token, and secret:example.token. The host bridge accepts only the exact granted HTTP host, and secret calls can read only the exact granted alias.

Unparameterized grant keys are written directly:

toml
[[capabilities]]
scope = "noteRead"
 
[[capabilities]]
scope = "noteWrite"
 
[[capabilities]]
scope = "blockWrite"
 
[[capabilities]]
scope = "pluginState"

noteRead gates note reads, noteWrite gates note write proposals, blockWrite gates block write proposals, and pluginState gates plugin-scoped JSON state. dashboardRead is reserved for future workspace-aware plugin surfaces; current Builder-created apps are offline and state-only. readVault is accepted as a note-read capability for current host functions, but new local plugins should prefer the narrower noteRead grant when they only read note content.

Note and block host helpers only accept vault-relative Markdown note paths under notes/, projects/, or people/. They reject absolute paths, whitespace padding, ./.. traversal, hidden path segments, backslashes, empty segments, non-.md files, and other vault roots before reading or proposing a write.

Host error handling

Host helpers return Result<T, wrengle_plugin_sdk::Error>. Missing grants, denied proposals, invalid requests, stale write bases, and host failures are not panics; handle them in plugin code.

rust
fn update_counter() -> Result<ActionResult, String> {
    match host::state_get("counter") {
        Ok(result) => {
            let value = result.value.unwrap_or_else(|| serde_json::json!(0));
            host::state_set("counter", value).map_err(|error| error.to_string())?;
            Ok(ActionResult::summary("Updated plugin state"))
        }
        Err(Error::CapabilityDenied(message)) => Ok(ActionResult::summary(format!(
            "Missing pluginState grant: {message}"
        ))),
        Err(error) => Err(error.to_string()),
    }
}

When a grant is missing, the host returns a capabilityDenied envelope and the SDK maps it to Error::CapabilityDenied. Running without a grant does not bypass the host API.

Write proposal flow

noteWrite and blockWrite do not let plugin code write directly. They let the plugin ask Wrengle to prepare app-mediated write proposals:

  1. The plugin declares noteWrite or blockWrite.
  2. The user grants that capability for this local plugin install.
  3. The action calls host::write_note or host::write_blocks.
  4. The host returns pendingUserApproval with a proposal id and preview data.
  5. Wrengle shows the proposal in the action dialog.
  6. The user approves or denies the proposal.
  7. Approved note proposals re-check the current note before saving. Conflicts are reported instead of overwriting silently.

host::write_note sends a complete raw Markdown replacement candidate. Wrengle previews and conflict-checks that candidate, but it does not reinterpret plugin noteWrite payloads as structured block ops. Prefer host::write_blocks for list, checklist, table, or other block-precise edits.

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

View network boundary

Manual/developer view-only plugins use local HTML and JavaScript inside a sandboxed iframe. The view bridge currently sends theme and selected-text messages only. Direct view JavaScript network calls from those manual views, such as fetch and XMLHttpRequest, are not controlled by the httpHosts grant. httpHosts gates only Wrengle host-bridge HTTP made by WASM actions. Builder-created offline apps are also plugin views, but they are checked against network access and remain offline.

docs / plugins/sdk-and-creatorAll documentation