Quick start

Applies to: StablEdge Desktop 0.4.4, plugin manifest protocol v3

This page builds a minimal workspace plugin from nothing: a page that can keep a draft and let the Agent read it. Nothing here needs a worker or a network permission.

Start with the smallest architecture

Ask for the capabilities this need requires, and do not reserve any for later:

NeedUse
Panels, forms, boards, visualizationsWorkspace interface (Workspace)
Parsing, conversion or background computation that belongs to the pluginPlugin Worker
Heavy capabilities shared by several plugins, each on its own versionGlobal Worker service
The Agent reading or changing plugin stateA Workspace plus a bundled Skill
A little settings and index datastorage.kv
Documents, images, audio and media files, and large statestorage.blob
An external HTTP APInet.fetch on the interface side, or host.net.fetch on the worker side

The recommended order of growth is Workspace → plugin Worker → global Worker service. A plugin that only displays things should not create a placeholder worker.

Folder layout

<plugin directory>/
├── plugin.json
└── payload/
    ├── ui/
    │   ├── index.html
    │   ├── app.js
    │   └── styles.css
    ├── i18n/
    │   ├── zh-CN.json
    │   └── en-US.json
    ├── resources/
    │   └── icon.png        # Required: 200×200 PNG, ≤ 500 KiB
    └── agent/
        └── SKILL.md

Every entry point and runtime asset must live under payload/ and appear in the files of the final plugin.json. Do not put node_modules, build caches, test data, secrets or absolute paths from your machine into the package.

Writing plugin.json

{
  "schema_version": 3,
  "plugin_id": "com.example.my-notes",
  "name": "My Notes",
  "version": "0.1.0",
  "minimum_app_version": "0.4.4",
  "icon": { "resource_id": "icon" },
  "contributions": {
    "resources": [
      { "id": "icon", "path": "payload/resources/icon.png", "media_type": "image/png" }
    ],
    "workspaces": [
      {
        "id": "main",
        "title": "My Notes",
        "entrypoint": "payload/ui/index.html",
        "bridge_version": "1.0",
        "allowed_placements": ["replace_files", "replace_editor", "replace_both"],
        "default_placement": "replace_editor"
      }
    ],
    "skills": [{ "id": "agent", "entrypoint": "payload/agent/SKILL.md" }]
  },
  "workers": [],
  "requested_permissions": [
    { "permission": "agent.context", "worker": "host", "optional": false },
    { "permission": "agent.events", "worker": "host", "optional": false },
    { "permission": "storage.kv", "worker": "host", "optional": true }
  ],
  "required_services": [],
  "platforms": ["windows-x86_64", "macos-aarch64", "macos-x86_64"],
  "files": {},
  "publisher": "Example Inc."
}
  • plugin_id cannot change once published; a reverse domain name is recommended.
  • agent.context, agent.events and storage.kv are all workspace-scoped permissions granted per workspace, so you can write "worker": "host" directly and do not need a worker for them.
  • When there is a workspace, skills and agent.context must appear as a pair.
  • files can stay empty while you develop; the packager computes the SHA-256 of every file and writes them in.
Do not copy the minimum_app_version (the template names an earlier version) or the placeholder signature string from the repository template. Put the lowest version you actually tested in minimum_app_version, and leave signature out of an unsigned development package, because the packager generates it.

Writing the interface

The interface must wait for stabledgePlugin.ready() before calling anything else. Only after the first context.publish succeeds does the host mark the workspace as available to bind to an Agent.

async function initialize() {
  const host = await window.stabledgePlugin.ready();
  const draft = (await window.stabledgePlugin.storage.get("draft")) ?? "";
  render(draft);
  await window.stabledgePlugin.context.publish({
    version: 1,
    draft,
    available_actions: [{ name: "draft.set", payload: { text: "string" } }],
  });
}

if (window.stabledgePlugin) void initialize();
else window.addEventListener("stabledge:ready", initialize, { once: true });

window.stabledgePlugin.events.on("agent.custom", async ({ event, payload }) => {
  if (event !== "draft.set" || typeof payload?.text !== "string") {
    showError("Unsupported action");
    return;
  }
  await window.stabledgePlugin.storage.set("draft", payload.text);
  render(payload.text);
});
When you cannot handle an Agent action, show a clear message in the interface instead of calling throw. An uncaught exception only leaves the page half initialized, and you cannot see why.

Writing the Skill description

payload/agent/SKILL.md tells the Agent which context the plugin publishes and which actions it accepts (such as draft.set and its parameters). Keep it in step with the available_actions defined in the interface.

Packaging and installing

  1. Open plugin creation mode in the desktop app and choose the <plugin directory>.
  2. Select Check project and fix the errors it reports about the manifest, icon, file set and permissions.
  3. Select Pack project to produce the .seplugin, or install straight into the preview environment to debug.
  4. Double-clicking the .seplugin opens the install preview; confirm the permissions to install. A new plugin is disabled, and you enable it yourself in the plugin list.

A package without an official signature installs as a developer build, so first turn on Allow unsigned plugins in Settings → Plugins → Install and signing. See Packaging, signing and publishing.

Checklist

  • Every capability has a matching manifest declaration and a way for you to grant it.
  • The interface does not reach system paths, raw network access or desktop app internals directly.
  • Persistence, external data and Agent interaction all account for failure, revoked permissions and reopening the plugin.
  • There is no empty worker that exists only to hold a permission but claims callable business methods.
On this page