Example Plugin
Moldavite pluginInsert a timestamp and show the current note's word count.
Add sandboxed commands with narrow, host-enforced access to the editor, unlocked Markdown, approved HTTPS hosts, trusted forms, and plugin-owned Keychain secrets.
Search the directory below, then use Install in Moldavite to open the app directly on that plugin. Moldavite fetches the registry because you clicked the install link, shows the permissions before installation, and asks Rust to verify both SHA-256 hashes before the shared staged installer makes files visible. A newly installed plugin stays disabled until you grant its permissions. Changed files always require a fresh grant. You can also browse from Settings → Plugins.
Insert a timestamp and show the current note's word count.
Publish the active note as a WordPress draft, with safe updates on re-publish.
No plugins match that search. Try a name, author, or permission such as
notes.read.
Moldavite opens a themed About this plugin dialog with the validated description, commands, setup instructions, permissions, and hosts. Reopen it later with the ⓘ action on the installed card, then enable and grant only if you trust the plugin.
Plugins ship alongside opt-in local semantic search, the read-first MCP and agent-ready Forge tools, external-edit conflict copies, and graph clustering that pulls connected components together while keeping orphan notes peripheral. The user guide covers those app features end to end.
Each plugin executes as an ES module in its own Web Worker with no DOM, Zustand, and no raw
Tauri IPC. Before plugin code is evaluated the worker scope is reduced to an
allowlist: the language built-ins, console, timers,
crypto, text encoding, URL, Blob, and the
postMessage channel. Everything else is gone: fetch,
XMLHttpRequest, WebSocket, EventSource,
importScripts, nested workers, caches, indexedDB, and
WebAssembly among them. Because it is an allowlist, capabilities shipped by
future browser capabilities begin denied and remain outside the sandbox until they are
allowlisted. Network access goes through the permissioned net.fetch method.
The worker proxy rejects undeclared calls early for useful errors, but Moldavite repeats permission and argument checks in the host. The host check is the security boundary. Unknown RPC methods are rejected.
A granted plugin may read every unlocked note, send selected data to approved hosts, read the active editor, or store credentials, depending on its permissions. Users should enable only plugins they trust, and authors should request the smallest useful surface.
Create a folder under the active Forge with exactly this shape:
<Forge>/.plugins/my-plugin/
├── manifest.json
├── plugin.js
└── README.md # optional, recommended for distribution
id.register(api) export.
There is no build or package requirement. A dependency-free plugin.js works as
written. If you bundle dependencies, distribute the final self-contained ES module because
Moldavite loads only that one entry file.
{
"id": "my-publisher",
"name": "My Publisher",
"version": "1.0.0",
"author": "Your Name",
"description": "Publishes the active note to an approved service.",
"apiVersion": 2,
"minAppVersion": "1.6.0",
"permissions": [
"editor",
"ui",
"notes.read",
"net.fetch",
"secrets"
],
"allowedHosts": ["api.example.com"],
"commands": [
{ "id": "configure-publisher", "label": "Configure publisher" },
{ "id": "publish-note", "label": "Publish active note…" }
],
"instructions": [
"Enable the plugin and approve its permissions.",
"Press `Cmd+P` and run **Configure publisher** first.",
"Open a note, then run **Publish active note…**."
]
}
| Field | Required | Meaning |
|---|---|---|
id |
Yes | Must equal the folder name; lowercase ASCII letters, digits, and hyphens; begins with a letter or digit; maximum 64 characters. |
name |
Yes | User-facing plugin name shown in Settings and trusted prompt chrome. |
version |
Yes | User-facing version. Consent also pins exact file bytes, so a version bump alone is not the integrity boundary. |
apiVersion |
Yes | Use 2 for this reference. Older manifests remain compatible. |
author |
No | Display metadata. |
description |
No | Display metadata; explain what the plugin does and where data may go. |
minAppVersion |
No | Informational metadata. Moldavite leaves the semver value unenforced, so plugins need their own runtime guard when one is required. |
permissions |
No | Supported capability strings from the permission table below. |
allowedHosts |
With net.fetch |
Non-empty unique array of exact lowercase public DNS hostnames. No scheme, port, path, IP, single-label name, localhost label, or wildcard. |
commands |
No |
Up to 50 { "id", "label" } entries shown before enable. Each id must
match api.commands.add; ids are limited to 128 characters, labels to
200, and duplicate ids are invalid.
|
instructions |
No |
Up to 20 setup/use step strings, 500 characters each. The post-install dialog
renders inline **bold** and backtick code; other text remains literal.
|
Unknown top-level fields and incorrectly typed values invalidate the manifest. Host matching
is exact. A grant for api.example.com covers only that hostname and its default
port. The permission sheet displays every manifest host.
With the current API, declaring net.fetch requires at least one
allowedHosts entry even if your plugin also asks for user-supplied site hosts
at runtime. Runtime grants extend that manifest list, and the manifest must still pass
validation.
Every successful in-app install opens a themed
About this plugin dialog from this validated metadata, and the installed
card's ⓘ button reopens it at any time. Without instructions, Moldavite
generates a short enable-and-palette flow from the description and command list. Legacy
manifests remain compatible and use runtime-registered commands as a display fallback after
enable.
plugin.js is an ES module whose default export receives the API. Register
commands during startup; command handlers may be synchronous or asynchronous.
export default function register(api) {
api.commands.add({
id: 'inspect-active-note',
label: 'Inspect active note',
handler: async () => {
const note = await api.editor.getActiveNote();
if (!note) return;
await api.ui.toast(`Open: ${note.path}`, 'success');
},
});
}
Moldavite namespaces command ids as <plugin-id>:<local-id> in the
host. Keep local ids stable and unique within the plugin. Every API call except
commands.add is an asynchronous RPC. A command invocation that never settles is
rejected by the host after 30 seconds.
interface PluginAPI {
app: { version: string; apiVersion: 2 };
commands: {
add(command: {
id: string;
label: string;
handler: () => void | Promise<void>;
}): void;
};
editor: {
getActiveNote(): Promise<{
path: string;
title: string;
content: string; // editor HTML
} | null>;
insertText(text: string): Promise<void>;
};
ui: {
toast(
message: string,
kind?: 'info' | 'success' | 'error'
): Promise<void>;
prompt(options: {
title: string;
message?: string;
fields: Array<{
name: string;
label: string;
type: 'text' | 'password' | 'url';
placeholder?: string;
required?: boolean;
}>;
confirmLabel?: string;
}): Promise<Record<string, string> | null>;
};
notes: {
list(): Promise<Array<{
path: string;
title: string;
kind: 'daily' | 'weekly' | 'standalone';
folder: string | null;
}>>;
read(path: string): Promise<string>; // Markdown body
};
net: {
requestHostAccess(host: string): Promise<boolean>;
fetch(url: string, options?: {
method?: string;
headers?: Record<string, string>;
body?: string;
}): Promise<{
status: number;
headers: Record<string, string>;
bodyText: string;
bodyBase64?: string;
}>;
};
secrets: {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
delete(key: string): Promise<void>;
};
}
api.app.version: string
The running Moldavite app version.
api.app.apiVersion: 2
The API version selected by this manifest.
api.commands.add({ id, label, handler }): void
Registers a command in the palette and slash menu.
if (api.app.apiVersion !== 2) throw new Error('API v2 required');
api.commands.add({
id: 'hello',
label: 'Say hello',
handler: () => api.ui.toast(`Hello from Moldavite ${api.app.version}`),
});
editor.getActiveNote(): Promise<ActiveNote | null>
Returns the active note's Forge-relative path, display title, and live editor HTML, or
null when no note is open.
editor.insertText(text: string): Promise<void>
Inserts text at the current editor cursor. If no editor is active, Moldavite shows an error notification.
const active = await api.editor.getActiveNote();
if (active) {
console.log(active.path, active.content); // content is HTML
await api.editor.insertText('\nPublished from Moldavite.');
}
Key per-note state with path, since display titles can change. The current API
has no general note-write method. insertText is an explicit active-editor action
under the editor permission.
ui.toast(message, kind?): Promise<void>
Shows an app notification. Kind accepts info, success, or
error.
ui.prompt(options): Promise<Record<string, string> | null>
Opens a user-mediated Moldavite form. Submit returns strings keyed by field name; Cancel
or Escape returns null.
await api.ui.toast('Ready to publish', 'success');
const values = await api.ui.prompt({
title: 'Configure publishing',
message: 'Credentials are verified before saving.',
fields: [
{ name: 'site', label: 'Site URL', type: 'url', required: true },
{ name: 'password', label: 'Application Password', type: 'password', required: true },
],
confirmLabel: 'Verify and save',
});
if (!values) return;
The host allows one plugin prompt or host-consent dialog at a time and always displays Request from plugin: Plugin Name in trusted chrome above plugin-controlled copy. A form has 1–12 fields. Field names must be unique identifiers beginning with a letter; supported field types are text, password, and URL. Titles are limited to 200 characters, messages to 2,000, confirm labels to 80, names to 64, labels to 160, and placeholders to 300.
notes.list(): Promise<PluginNoteMetadata[]>
Lists daily, weekly, and standalone note metadata, including locked placeholders.
notes.read(path: string): Promise<string>
Reads the Markdown body of an exact listed, unlocked note path. Locked and unknown paths reject.
const notes = await api.notes.list();
const standalone = notes.find((note) => note.kind === 'standalone');
if (standalone) {
const markdownBody = await api.notes.read(standalone.path);
await api.ui.toast(`Read ${markdownBody.length} characters`);
}
Paths are Forge-relative, such as daily/2026-07-13.md or
notes/Projects/roadmap.md. The list's folder is relative to
notes/ for standalone notes and null otherwise. The read result is
the parsed Markdown body; YAML frontmatter is not included. The plugin receives no arbitrary
filesystem-read capability.
net.requestHostAccess(host: string): Promise<boolean>
Returns true for an already approved exact host, otherwise asks the user. Denial returns false; malformed or non-public hostname forms reject.
net.fetch(url, options?): Promise<PluginFetchResponse>
Makes a host-performed HTTPS request after checking the effective exact-host allowlist.
const site = new URL('https://notes.example.com');
const approved = await api.net.requestHostAccess(site.hostname);
if (!approved) return;
const response = await api.net.fetch(`${site.origin}/api/drafts`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ title: 'From Moldavite' }),
});
if (response.status >= 400) throw new Error(response.bodyText);
requestHostAccess uses the manifest hostname validator: no scheme, port, path,
wildcard, IP literal, single-label name, or label named localhost. The consent
dialog names the plugin and exact host. Existing manifest or runtime approval returns
true without another prompt.
Runtime approval is stored in Moldavite's per-Forge plugin grant record, outside the plugin files. The effective allowlist is the union of manifest and user-approved hosts. Users can revoke a runtime host under Settings → Plugins → View permissions; the next request and every redirect hop immediately use the reduced union.
CONNECT, TRACE, and
TRACK are blocked.
Location is resolved and checked before
the next request, with at most five redirects.
Accept, Accept-Language, and Content-Type when a
body remains. Authorization and cookies are not forwarded.
Response headers are restricted to content-type, content-length,
etag, last-modified, link, retry-after,
x-wp-total, and x-wp-totalpages. set-cookie is never
exposed. Text, JSON, XML, JavaScript, and form bodies are decoded in bodyText;
non-text responses also include bodyBase64.
secrets.get(key): Promise<string | null>
Returns the stored string or null.
secrets.set(key, value): Promise<void>
Stores a string in this plugin's macOS Keychain namespace.
secrets.delete(key): Promise<void>
Deletes the entry when present and otherwise succeeds.
await api.secrets.set('api-token', token);
const saved = await api.secrets.get('api-token');
if (saved) {
// Use it through api.net.fetch, then remove it when no longer needed.
await api.secrets.delete('api-token');
}
The Keychain service is Moldavite. The host constructs the account as
plugin:<plugin-id>:<key>, so a worker cannot choose another
plugin's namespace. Keys are 1–128 characters, begin with a letter or digit, and then use
letters, digits, dots, underscores, or hyphens. Secret values are never listed or included
in Forge, settings, plugin, ZIP, or encrypted-backup exports.
| Permission | What it grants |
|---|---|
| None |
app, commands.add, and the current
ui.prompt.
|
editor |
Read active-note path/title/HTML and insert text at the cursor. |
ui |
Show toast notifications. |
notes.read |
List note metadata and read unlocked Markdown bodies. |
net.fetch |
Request runtime hosts and ask Moldavite to call exact approved HTTPS hosts. |
secrets |
Read, write, and delete this plugin's Keychain entries. |
Consent is pinned to a SHA-256 hash over the raw manifest.json bytes, a
separator, and the plugin.js bytes. Editing source, permissions, or
allowedHosts therefore changes the hash and reopens the permission sheet even
if version did not change.
Runtime host grants are deliberately separate: they survive a plugin version/hash re-grant, remain visible and individually revocable, and are forgotten with the plugin's consent record when it is uninstalled. They leave files and the content hash unchanged.
Disabling a plugin, uninstalling it, switching Forges, a worker crash, or an unreadable worker message terminates the worker, removes its commands, and rejects pending command invocations. Malformed manifests are marked invalid and never executed.
Moldavite bundles a dependency-free first-party plugin under
src-tauri/example-plugin/moldavite-wordpress/. Install it from
Settings → Plugins, enable it, and inspect its manifest, source, and README
as a complete reference.
ui.prompt for an HTTPS
site URL, username, and Application Password.
net.requestHostAccess, and verifies
/wp-json/wp/v2/users/me?context=edit before saving configuration through
secrets.set.
editor.getActiveNote and
sends the live editor HTML to /wp-json/wp/v2/posts as a draft.
PUT to update the existing post. The success notification includes the edit
URL.
Self-hosted WordPress and WordPress.com Jetpack/Atomic sites work when they expose the standard REST API and Application Passwords. WordPress.com Simple sites require OAuth with a registered client ID; the reference plugin intentionally does not embed or fake one and does not support those sites.
manifest.json, the final self-contained plugin.js, and a README.
plugins/<id>/, update registry.json with
the exact SHA-256 hashes and metadata, and open a pull request for review.
<Forge>/.plugins/ remains available. Enable state and consent
are per Forge.
version for human
clarity. Any byte change already invalidates the content-hash grant and requires fresh
consent.
secrets.delete for every
known key when users need credential cleanup before uninstalling.
Existing manifests with "apiVersion": 1 remain valid and receive the original
app, commands, editor, and
ui.toast surface, with api.app.apiVersion === 1. They do not need
a source or manifest migration. Use "apiVersion": 2 for trusted prompts,
Forge note reads, networking, and Keychain secrets.