Plugin API and Permissions
The supported Manifest v2 worker, pane, editor, filesystem, network, and SSH contracts in Zync.
This reference describes the current desktop 2.34.0 source and SDK
2.1.0-beta.4. The native host validates every privileged operation against the
installed package, grants, runtime, and applicable pane or handle. A TypeScript
type, a known permission name, or a successful preflight is not an authority grant.
Manifest and engine ranges
Section titled “Manifest and engine ranges”Require manifestVersion: 2, a semantic plugin version, a publisher, an ID
under that publisher namespace, and both engines.zync and engines.pluginApi.
Use the starter manifest.
| Field | Purpose |
|---|---|
runtime.entry |
Built worker script inside the package |
contributes.commands |
Command IDs and titles registered by the worker |
contributes.paneKinds |
Pane IDs, titles, packaged HTML entries, and optional allowMultiple |
permissions.required |
Capabilities necessary for installation |
permissions.optional |
Capabilities the user may decline or revoke |
homepage, support, privacyPolicy |
Optional HTTPS URLs without embedded credentials |
icon |
Optional packaged icon path |
Unknown required permissions fail validation. Unknown optional permissions may
warn in preflight but are unavailable until the host supports them. Each declared
permission needs an honest reason; do not describe remote execution as read-only.
An arbitrary scope string cannot create support for a new resource scope.
SDK and host API versions are separate. ^2.0.0 accepts a compatible API 2.1 host;
features introduced in 2.1 must declare ^2.1.0. Optional feature detection still
matters for APIs added to later desktop releases without a new API major.
Supported worker operations
Section titled “Supported worker operations”Use import type { ZyncWorkerApi } from '@zync-sh/plugin-sdk/worker' in TypeScript
and declare the host-provided zync. Compile the types away. Handle rejected
promises and permission denial at the action boundary.
| API | Required permission | Behavior |
|---|---|---|
on('ready', callback) |
None | Start registrations when the worker bridge is ready |
commands.register(id, title, handler) |
ui.commands.register |
ID must be declared in contributes.commands |
panel.register(id) |
ui.pane.register |
Registers a declared pane; HTML is read from its package entry |
panel.onMessage, panel.postMessage |
Registered pane ownership | Exchange bounded JSON with a live pane of this plugin |
ui.notify, ui.onNotifyAction |
ui.notifications.emit for emission |
Toast/inbox notifications and action callbacks |
ui.confirm(options) |
ui.dialog.confirm |
A Zync-owned confirmation; returns a boolean |
storage.get, storage.keys |
filesystem.pluginData.read |
Plugin-private, device-local string values |
storage.set, storage.delete |
filesystem.pluginData.write |
Bounded private storage; not a credential vault |
network.fetch(url, { accept? }) |
network.fetch with declared hosts |
Bounded public HTTPS GET through the host |
filesystem.pickFile, pickDirectory, readText, list |
filesystem.external.read |
Host picker creates runtime-bound read handles |
filesystem.pickWriteFile, writeText |
filesystem.external.write |
User-selected write handle and atomic text write |
sshFilesystem.list, readText |
ssh.filesystem.read |
Relative read/list operations under the bound server’s home |
sshCommand.execute |
ssh.command.execute |
Structured command arguments on the pane’s POSIX SSH server; API 2.1 |
Optional sshTerminal.context, prepare |
ssh.terminal.open |
Propose a host-owned terminal; explicit launch approval remains necessary |
The manifest recognizes some capabilities and contribution names that do not yet have public worker routes. Dashboard registration, arbitrary status/sidebar/settings injection, general clipboard access, local-network fetch, and legacy terminal input APIs must not be advertised as available merely because their permission IDs appear in the catalog. The installed SDK worker interface and implemented host routes define the supported API.
Pane messaging and lifecycle
Section titled “Pane messaging and lifecycle”Type the pane with ZyncPaneApi from @zync-sh/plugin-sdk/pane. Its injected
window.zync.pane supports postMessage(message) and onMessage(callback).
The subscription returns an unsubscribe function. Newer hosts also expose
optional isVisible() and onVisibilityChange(callback).
The worker receives { paneInstanceId, message }. Use that host-supplied instance
ID when calling SSH APIs or replying. Do not trust a connection ID or arbitrary
program name supplied by the UI. Maintain independent state for each pane.
Messages must be JSON-compatible. They are limited to 64 KiB, 16 levels of nesting, 128 keys per object, and 4,096 items per array. Large logs and tables need bounded pages or summaries. There is no raw PTY stream over this channel.
The pane is an isolated iframe, not a host-DOM extension. Use the injected bridge; do not implement your own parent-window IPC or assume an origin gives native authority. Dispose subscriptions and cancel stale UI work when the pane closes.
Notifications
Section titled “Notifications”For notification payloads, declare ui.notifications.emit and call the worker API:
zync.on('ready', async () => { await zync.ui.notify({ type: 'info', message: 'Plugin ready.', channel: 'toast', });});For an action, supply JSON actions: [{ id: 'retry', label: 'Retry' }] and register
ui.onNotifyAction. Only retry operations that are safe to repeat. A notification
action is not a substitute for confirming a destructive operation.
Files and private storage
Section titled “Files and private storage”Picker handles are opaque, runtime-bound, and operation-specific. A read handle cannot authorize writing. Do not persist handles across reloads or turn a relative path into an arbitrary local path. The host restricts traversal, links, special files, and sensitive application/credential locations. Remote filesystem reads are bound to the pane’s SSH connection and rooted at its home directory.
Private storage is scoped to the plugin on the device, not to a server. Namespace your own per-server data carefully, without using secrets as keys. Use versioned, backward-readable records so a rollback can still read them. Uninstall retains this store unless the user chooses to delete data.
Public HTTPS reads
Section titled “Public HTTPS reads”Declare the hosts needed by network.fetch, for example this permission entry:
{ "id": "network.fetch", "reason": "Read public release information from our service.", "hosts": ["api.example.com"]}Hosts are DNS names, not URLs, ports, or IP addresses. A wildcard such as
*.example.com does not include the apex example.com. Prefer exact names.
The broker supports GET and an optional accept value, not arbitrary methods,
authorization headers, cookies, or POST bodies. Private/reserved destinations and
nonstandard HTTPS ports are rejected; redirects are revalidated.
The result contains status, finalUrl, optional contentType, body, and
bodyEncoding (utf8 or base64). Check status and encoding before parsing.
Do not design a plugin around direct fetch, WebSocket, or access to local services.
SSH commands and confirmations
Section titled “SSH commands and confirmations”sshCommand.execute(paneInstanceId, { program, args, expectedConnectionToken? })
runs with the connected SSH account’s full authority. Arguments are individually
quoted for a POSIX shell; they are not a sandbox and do not undergo automatic
shell expansion. Return fields are stdout, stderr, exitCode, and
connectionToken.
The permission grant does not show a fresh confirmation for every command.
Your worker must request ui.confirm before destructive actions. Use the
connection token from an earlier read to bind the confirmed action to the same
connection. Do not retry a mutation automatically after a timeout or lost reply.
async function restartService(paneInstanceId) { const context = await zync.sshCommand.execute(paneInstanceId, { program: 'systemctl', args: ['is-active', 'example.service'], }); const confirmed = await zync.ui.confirm({ title: 'Restart example.service?', message: 'This interrupts the service on this pane\u2019s server.', confirmLabel: 'Restart', }); if (!confirmed) return; return zync.sshCommand.execute(paneInstanceId, { program: 'systemctl', args: ['restart', 'example.service'], expectedConnectionToken: context.connectionToken, });}The example assumes that the remote account is already allowed to manage this
specific service. It does not request a password or add sudo. Callers must catch
errors, inspect the exit code, and refresh state. Closing/rebinding the pane or
disconnecting cancels the local channel; remote side effects may already have
happened. Validate application-level values before building argument arrays and
allowlist the operations your UI can request.
Host-owned terminals
Section titled “Host-owned terminals”The optional terminal integration in SDK 2.1.0-beta.4 lets a worker propose an
interactive program and a pane reserve a rectangular surface. The host owns
xterm, launch approval, input, and output. The plugin cannot silently launch a
terminal, inject keystrokes, or read its PTY output.
Use sshTerminal.context and carry its token into sshTerminal.prepare. Send the
returned offer to the originating pane and bundle mountTerminalSurface from
@zync-sh/plugin-sdk/terminal. Offers expire after 60 seconds. An axis-aligned slot
must be at least 240 by 100 CSS pixels. Dispose the surface on teardown; request a
new offer after exit. registerTerminalOverlay registers actual popup rectangles
and must also be disposed when they close.
Feature-detect the optional worker API and check surface.ready. Keep a fallback
for older hosts and set a tested desktop minimum for release. The presence of SDK
types does not prove an older packaged app supports these routes. See the
SDK terminal example.
Editor providers
Section titled “Editor providers”Editors use type: "editor-provider", the editor object, and the
editor.provider.register permission. They are not ordinary pane registrations.
The SDK’s general ManifestV2 interface does not yet enumerate the specialized
editor fields; use the native contract and
Zedit manifest
as references and validate the installed result.
Listen with zyncEditor.onMessage before calling emitReady. On document open,
retain the supplied docId. Use it with emitChange({ docId, content }),
emitDirtyChange(dirty, docId), and reportStatus({ docId, line, column, language? }).
Cursor coordinates are one-based. Request saves through requestSave; wait for
the host result before treating content as saved. Reject stale document results
when another file opens. Declare only capabilities actually implemented.
Current limits
Section titled “Current limits”These are ceilings, not workload targets. Stay well below them and handle errors.
| Resource | Current bound |
|---|---|
| ZIP download / expanded package | 25 MiB / 100 MiB |
| Archive entries / individual package file | 2,048 / 20 MiB |
| Manifest / pane entry HTML | 256 KiB / 512 KiB |
| SDK pane asset preflight | 2 MiB per asset; stricter than the native package file ceiling |
| Private storage | 256 keys, 64 KiB per value, 1 MiB serialized store |
| Local file text / directory list | 2 MiB / 500 entries |
| Public HTTPS response | 2 MiB, at most 3 redirects |
| SSH command | 20 seconds, 2 MiB combined output, one concurrent command per pane |
Source contracts: SDK types and validator, native plugin modules, and frontend broker.