Skip to content
ZyncDocsGitHub Download Zync

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.

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.

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.

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.

For notification payloads, declare ui.notifications.emit and call the worker API:

Worker notification
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.

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.

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.

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.

Worker helper; requires ssh.command.execute and ui.dialog.confirm
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.

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.

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.

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.