Every plugin package has a plugin.json in its root: the plugin manifest. The host validates it against the v3 JSON Schema at install, then runs the cross-field checks (permission combinations, file hashes, the icon and so on). Any field the schema does not define is refused.
Top-level fields
| Field | Required | Type and constraints | Notes |
|---|---|---|---|
schema_version | Yes | constant 3 | Manifest protocol version. |
plugin_id | Yes | ^[a-z][a-z0-9.-]{2,63}$ | Globally unique, unchanging plugin identifier; a reverse domain name is recommended, such as com.example.my-notes. |
name | Yes | string, 1–80 | Plugin name. |
display_name | No | string, 1–80 | Name shown in the interface; name is used when this is absent. |
description | No | string, 1–200 | One-line summary, shown in the plugin list and the install preview. |
plugin_type | No | workspace | knowledge | skill | integration | utility | Plugin category: workspace, knowledge source, Skill, integration, utility. |
icon | See notes | { "resource_id" } | Points at a PNG resource in contributions.resources. New packages must provide it; see Icon. |
i18n | No | object | Translations the plugin carries itself; see Translations. |
version | Yes | semantic version, such as 1.2.0, 1.2.0-beta.1 | Plugin version. |
minimum_app_version | Yes | string, 5–32 | Lowest desktop version required; installing on anything older is refused. |
contributions | Yes | object | The interfaces, Skills and resources the plugin provides; see Contributions. |
workers | Yes | array, ≤16 | Background worker processes; write [] when there are none. |
triggers | No | array, ≤32 | Scheduled or lifecycle triggers. |
event_channels | No | array, ≤16 | Long-lived event channels. |
credentials | No | array, ≤16 | Credentials the host keeps. |
requested_permissions | Yes | array, ≤64 | Permissions requested; see Permissions. |
required_services | Yes | array, ≤32 | Services the plugin depends on; write [] when there are none. |
ai_runtime | No | object | An AI runtime the host runs; must come together with the runtime.ai permission and is open to the official source only. |
platforms | Yes | at least one of windows-x86_64 | macos-aarch64 | macos-x86_64 | Supported platforms; installing on a platform that is not listed is refused. |
files | Yes | object: path → 64-character lowercase hex | SHA-256 of every file in the package, forming the sealed file set. |
publisher | Yes | string, 1–120 | Publisher name. |
publisher_key_id | No | ^[A-Za-z0-9._-]{3,128}$ | Identifier of the signing public key; comes as a pair with signature. |
signature | No | string, 40–4096 | Manifest signature; comes as a pair with publisher_key_id. The packager generates it, so never write it by hand. |
Local identifiers
Every id in a manifest (workspace, worker, Skill, resource, trigger and so on) follows one rule: ^[a-z][a-z0-9_-]{1,47}$, so it starts with a lowercase letter and is 2–48 characters long. Two contributions of the same kind cannot share an id.
Icon
- Declare a resource in
contributions.resourceswithmedia_typeset toimage/png, then refer to it fromicon.resource_id. - The image must be exactly 200×200 pixels, no larger than 500 KiB, and listed in
files. - At pack time a missing icon, a file that is not a PNG, the wrong size or an icon left out of
filesrefuses the package outright (PLUGIN_MANIFEST_INVALID); at install an invalid icon resource reportsPLUGIN_ICON_INVALID. - Older packages that have shipped without an icon still install, but a 200×200 icon is required the next time you pack.
- Images and fonts used by the workspace and top bar pages go straight under
payload/ui/and do not need to be declared inresources.
Translations
i18n lets a plugin carry its own translations of its name, summary and interface text. Writing into the desktop app's language packs is neither needed nor allowed.
| Field | Required | Constraints | Notes |
|---|---|---|---|
default_locale | Yes | ^[a-z]{2}(-[A-Z]{2})?$ | Default language, such as zh-CN. |
languages | Yes | array, 1–32 | One entry per language. |
languages[].locale | Yes | as above | Language tag. |
languages[].label | Yes | 1–40 | Language name, such as "Simplified Chinese". |
languages[].display_name | Yes | 1–80 | The plugin's display name in that language. |
languages[].description | No | 1–200 | The summary in that language. |
languages[].workspaces | No | object: workspace id → { "title" } | Workspace titles in that language. |
languages[].ui_catalog | No | payload/i18n/*.json | Interface text dictionary, read in the interface with i18n.t(). |
At least one contribution
A manifest must provide at least one of knowledge_providers, workspaces, skills or resources in contributions. Because a new plugin has to declare its icon through resources, a plugin with only a top bar item and no workspace is valid too.
Contributions
Every entry point, resource and Skill file must live under payload/ and appear in files.
workspaces: the workspace interface
Up to 16 entries. Each is an HTML page that runs in a sandboxed iframe; the API is described in Workspace and Bridge API.
| Field | Required | Constraints | Notes |
|---|---|---|---|
id | Yes | local identifier | Workspace identifier. |
title | Yes | 1–80 | Workspace title. |
entrypoint | Yes | payload/ui/*.html | Entry page. |
bridge_version | Yes | constant "1.0" | Workspace Bridge version. |
allowed_placements | Yes | at least one of replace_files | replace_editor | replace_both | Allowed placements: replace the file area, replace the editor area, or both. |
default_placement | Yes | as above, a single value | Default placement. |
worker | No | references workers[].id | The worker the interface calls through worker.invoke. |
drops | No | object | Accepts dropped files; requires storage.blob. The host stores the file in the plugin's blob storage and then tells the interface, so the plugin never receives a system path. |
drops.extensions | Yes* | 1–16 items, ^[a-z0-9]{1,16}$ | Accepted extensions, without the dot. |
drops.max_files | Yes* | 1–16 | Most files in one drop. |
drops.max_bytes | Yes* | 1–1073741824 | Most bytes in one drop. |
capture_providers | No | ≤8 items | Provide screenshots of the interface to the Agent; requires agent.capture and storage.blob. |
capture_providers[].id | Yes* | local identifier | Screenshot provider identifier. |
capture_providers[].description | Yes* | 1–160 | Tells the Agent what the screenshot shows. |
capture_providers[].max_long_edge | Yes* | 256–4096 | Longest edge of the screenshot in pixels. |
connect | No | ≤64 http(s) origins | See Network declarations. |
Fields marked * are required whenever their parent object appears.
topbar_items: top bar items
Up to 16 entries, shown in the top bar reserved for workspace plugins. An item whose side is full takes the whole row, and plugins in the same section cannot be enabled at the same time.
| Field | Required | Constraints | Notes |
|---|---|---|---|
id | Yes | local identifier | Top bar item identifier. |
side | Yes | left | center | right | full | Which section it sits in. |
label | Yes | 1–32 | Text shown. |
tooltip | No | 1–160 | Hover text. |
priority | No | -100–100, default 0 | Sort order within the same section. |
workspace | No | references a workspace id | The workspace that opens when the item is clicked. |
worker | No | references a worker id | The worker that supplies the data; comes together with method. |
method | No | starts with ^topbar\. | Worker method the host calls on a timer. |
entrypoint | No | payload/ui/*.html | A small embedded page; comes together with bridge_version. |
bridge_version | No | constant "1.0" | Bridge version the embedded page uses. |
min_width | No | 48–600, default 48 | Minimum width in pixels. |
max_width | No | 48–600, default 280 | Maximum width in pixels. |
refresh_seconds | No | 1–300, default 15 | How often method is called. |
connect | No | ≤64 http(s) origins | See Network declarations. |
Each top bar item must take one of these three shapes:
- Static: only display fields such as
label, with noworker,method,entrypointorbridge_version. - Worker-driven:
workerandmethodtogether, with noentrypoint. - Embedded page:
entrypointandbridge_versiontogether, with nomethod.
skills: Skills
Each entry is { "id", "entrypoint" }, where entrypoint is payload/agent/*.md. A Skill file tells the Agent how to read the context the plugin publishes and which actions it may send. Skill files take part in hash and signature checks, but gain no extra tool permissions by doing so.
payload/agent/ holding only that file.resources: resources
Up to 64 entries, with all three fields required: id (local identifier), path (a file under payload/resources/) and media_type (in type/subtype form, 3–127 characters). Resources are files for the host to read, and are used today by the root icon and ai_runtime.
knowledge_providers: knowledge sources
Up to 16 entries, each { "id", "worker" }, where worker references workers[].id.
Network declarations (connect)
Workspaces and top bar items can list the origins they will reach in connect (only http:// or https:// plus a host and an optional port, with no path), so the install page can show them. Declaring connect means you must also request net.fetch with worker set to host, otherwise the install is refused. When you configure the address yourself, connect can be left out.
"connect": ["https://api.example.com"]
workers: Workers
Up to 16 entries. How a worker runs and the protocol it speaks are covered in Worker.
| Field | Required | Constraints | Notes |
|---|---|---|---|
id | Yes | local identifier | Worker identifier, referred to by permissions, workspaces and triggers. |
entrypoint | Yes | payload/worker/* | Entry script. |
protocol_version | Yes | constant "2.0" | Worker protocol version. |
isolation | Yes | sandboxed_plugin | dev_unrestricted | Isolation level. A dev_unrestricted worker runs unrestricted only after you grant This-computer mode. |
activation | Yes | on_demand | on_startup | Start when needed, or with the app. on_startup only starts the process and never runs business logic by itself. |
resource_limits.memory_mb | Yes | 64–2048 | Memory limit (MB). |
resource_limits.cpu_percent | Yes | 1–100 | CPU limit (%). |
resource_limits.max_inflight | Yes | 1–64 | Most requests handled at once. |
execution_policy | No | sandbox | trusted, default sandbox | trusted needs your confirmation at install, still runs in the sandbox and cannot start child processes. |
Set resource limits to what you actually need, and do not raise them blindly to work around a problem.
required_services: services you depend on
| Field | Required | Constraints | Notes |
|---|---|---|---|
service_id | Yes | ^[a-z][a-z0-9.-]{2,127}$ | Service identifier, such as com.stabledge.ocr. |
version | Yes | 1–64 | Semantic version range, such as ^1.0.0. |
worker | Yes | local identifier | The worker that calls the service; that worker must also request service.invoke. |
optional | No | boolean, default false | When true, a missing service does not block the install and the plugin has to degrade on its own. |
Background tasks, events and credentials
Use the three declarations below when a plugin has to run with no interface open and no Agent messages: scheduled sync, catching up once after startup or wake, and listening to a push stream. Background work is always scheduled by the host: when the time comes or an event arrives, the host calls the worker operation you declared. Do not run your own setInterval or keep your own socket inside a worker.
triggers
| Field | Required | Constraints | Notes |
|---|---|---|---|
id | Yes | local identifier | Trigger identifier. |
kind | Yes | interval | startup | resume | On an interval, after the app starts, or after the system wakes. |
worker | Yes | local identifier | The worker that runs it. |
operation | Yes | ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ | The operation to call; it must appear in the methods the worker returns at handshake. |
minimum_interval_seconds | See notes | 60–604800 | Required when kind is interval, and must be left out for the other kinds. |
default_enabled | No | boolean, default false | When false, you enable it yourself under Background tasks on the plugin detail page. |
catch_up | Yes | skip | once | After missing several cycles, do not catch up, or catch up at most once. |
"triggers": [
{
"id": "sync",
"kind": "interval",
"worker": "main",
"operation": "sync.incremental",
"minimum_interval_seconds": 900,
"default_enabled": false,
"catch_up": "once"
}
]
event_channels: event channels
The host keeps the connection open (connecting, heartbeat, reconnecting) and calls the declared operation once for every event it receives. The worker must request events.connect.
| Field | Required | Constraints | Notes |
|---|---|---|---|
id | Yes | local identifier | Channel identifier. |
kind | Yes | sse | websocket | webhook | webhook means a cloud Webhook relay: your own service receives a third-party webhook and pushes it to the host over wss://. |
worker | Yes | local identifier | The worker that handles events. |
operation | Yes | as for triggers | The operation that handles events. |
url | Yes | URI, ≤2048 | sse requires https:// and the other two require wss://. During local development http:// and ws:// are allowed for 127.0.0.1, localhost and [::1]. |
credential_refs | No | ≤8 items | Credential ids injected when the connection is made. |
protocols | No | ≤8 items | WebSocket subprotocols. |
default_enabled | No | boolean, default false | There is no separate switch for event channels in the interface yet, so this field decides whether the connection is made. |
Delivery is at least once: an operation that returns successfully confirms the event, while an error or a timeout replays it after reconnecting. Make your handling idempotent by event id.
credentials
You fill in a credential on the plugin detail page or complete an authorization, and the host keeps it in the system keychain. Workers, the Agent and the interface cannot read the plain value; they can only refer to it and let the host inject it into a request. A worker that declares credentials must request net.fetch or events.connect.
| Field | Required | Constraints | Notes |
|---|---|---|---|
id | Yes | local identifier | Credential identifier. |
label | Yes | 1–80 | Name shown to you. |
kind | No | static | oauth2, default static | A static secret or OAuth2. When oauth2, the oauth2 object is required. |
worker | Yes | local identifier | The worker that uses the credential. |
allowed_origins | Yes | 1–32 https:// origins | Injected only when the request target is in the list. |
injection.location | Yes | header | query | Where the value is injected. |
injection.name | Yes | ^[A-Za-z][A-Za-z0-9_-]{0,63}$ | Request header or query parameter name. |
injection.prefix | No | ≤64, empty by default | Value prefix, such as Bearer . |
OAuth2 credentials
The host handles PKCE, token refresh and rotation; on a 401 it forces a refresh and retries once.
| Field | Required | Constraints | Notes |
|---|---|---|---|
authorization_endpoint | Yes | https://, with no query or fragment | Authorization address. |
token_endpoint | Yes | as above | Address to exchange for a token. |
revoke_endpoint | No | as above | Address to revoke a token. |
client_id | Yes | 1–512 | Client identifier. |
scopes | No | ≤64 items | Scopes requested. |
callback_mode | Yes | custom_scheme | loopback | Callback style: a custom scheme, or a loopback address on this machine. |
client_auth | No | none | client_secret_post | client_secret_basic, default none | How the client authenticates. |
token_request_encoding | No | form | json, default form | Encoding of the token request body. json cannot be used when token_method is get. |
token_method | No | get | post, default post | Token request method. |
response | No | object whose values are JSON Pointers | For a non-standard response, say where access_token, refresh_token, expires_in, token_type, error, error_description and metadata are found. |
authorization_params | No | ≤32 key-value pairs | Extra parameters for the authorization request. |
token_params | No | ≤32 key-value pairs | Extra parameters for the token request. |
input, in a cursor, in a log, or in a long-lived cache in worker memory.ai_runtime
ai_runtime lets the host fetch source, build a Python environment and run a model runtime for you; it must come together with the runtime.ai permission. It is open to the official source only, and a package from a third-party publisher is refused outright. When a third-party plugin needs a model or heavy computation, use a global Worker service, or let the user run a local HTTP service.
files: the sealed file set
- Keys are relative paths separated by
/and starting withpayload/; values are the SHA-256 of the original file bytes (64 lowercase hex characters). - You cannot leave a file out or list an extra one, and absolute paths,
.., backslashes and symbolic links are not allowed. - Pack again after every change under
payload/; the old hashes and signature stop working at once. Do not editfilesby hand.
v1 and v2 manifests
The desktop app still installs v1 and v2 manifests, but only for older plugins and migration; new plugins always use v3.
| Version | What it offers |
|---|---|
| v1 | Knowledge source plugins only; top-level entrypoint and capabilities; a signature is required. |
| v2 | Adds workspaces and agent_integration, and capabilities grew workspace and Agent event capabilities; signature is required. |
| v3 | Replaces capabilities with contributions, workers and requested_permissions; supports the top bar, background tasks, credentials and service dependencies; allows unsigned developer packages. |