Files
nyaterm/docs/plugin-system.md
T
Kang 2ffbaf995d feat(plugins): introduce V1 plugin system with manifest and invocation support
Add a new plugin architecture allowing for dynamic plugin management and execution. This includes the implementation of plugin manifests, action validation, and parameter handling. Introduced new modules for plugin invocation, manifest parsing, and preferences management. Updated the core library to support plugin integration and added tests for plugin functionality. Localization updates for plugin management UI in multiple languages were also included.
2026-10-04 15:42:04 +08:00

16 KiB

NyaTerm plugins V1

Plugins provide command drafts and transform text explicitly supplied by the user. They cannot execute commands or access sessions, terminal output, history, clipboard, credentials, files, network or processes. NyaTerm implements this system independently under Apache-2.0; Zed's GPL extension architecture was a read-only architectural reference.

Install and use

Open Settings → General → Manage plugins and actions, or Plugins in the quick-command toolbar. Choose a directory containing plugin.toml, or a ZIP with that file at its root, then select Install. Installation copies and validates a managed snapshot; later edits to the source do not affect it.

The manager shows metadata, API/host requirements, origin (managed/development), status, contributed actions and actionable errors. Installed (disabled) means no contributions are available. Declarative plugins become Enabled; a live component becomes Running. Incompatible packages, loading failures and runtime faults have distinct states. A runtime fault revokes the instance's actions; reload it to retry. Failed replacement leaves the old plugin available.

  • Enable/Disable persists across restarts. Disable cancels calls and removes contributions immediately when the operation completes.
  • Update from source path above validates a package with the same plugin ID, prepares its replacement, then switches it. Local updates are explicit; there is no automatic networking or marketplace.
  • Reload recreates a managed instance. For a registered development directory, it first takes another validated snapshot of the source.
  • Uninstall (keep data) removes the managed package, preference and all contributions, retaining work/data/<plugin-id>. V1 guests have no KV or file access, so the SDK itself currently writes no persistent plugin data.

Select an action, fill its typed parameters, and choose Generate / transform. Text actions receive only text entered/pasted into their input box. The user must explicitly copy terminal selection and paste it there; plugins never subscribe to selection, terminal output or the clipboard. Results appear as ordinary editable text and can be copied. Text results can be reused as another text input.

For command results, Fill send box appends to an existing draft; Replace send-box draft explicitly replaces it. The send box opens, but neither action sends bytes, presses Enter, changes targets or chooses sessions. Stop any active send and select text mode first. Review the final draft and use NyaTerm's usual send controls yourself. Plugin contributions never modify saved QuickCommands.

For a local demonstration from a checkout:

rustup target add wasm32-unknown-unknown
python scripts/plugins/build.py --fixtures
cargo build --locked
cargo run -p nyaterm-app --bin nyaterm

The default build includes the MCP, RDP and VNC helpers beside the application. Open the manager and install target/plugin-examples/templates.zip, diagnostic-command.zip and text-tools.zip. Generate a directory-listing or diagnostic draft, edit its preview and explicitly fill the send box. For the text example, paste lines to sort or JSON to format, then copy/reuse the result. Disable and re-enable a plugin to check action removal/restoration. To exercise development reload, register a package directory instead of its ZIP, edit its manifest/template or rebuild its component, and reload. An invalid replacement reports an error while preserving the previous version. Uninstall removes its contributions; enabled preferences survive application restart.

Directories and data compatibility

The root is AppRuntime.data_dir()/plugins, so portable installations use their local data directory and installed stable/preview builds remain isolated.

Path Purpose
installed/<plugin-id>/ Validated managed package snapshot
staging/<transaction-id>/ Copy/extraction, replacement and rollback trees
work/data/<plugin-id>/ Retained, isolated data area; not exposed to V1 guests
dev/preferences.redb Isolated enabled preferences and canonical development-source registrations

The installed directory is the catalog. It is scanned at startup; there is no irreplaceable contribution index. Missing preferences default to disabled. Corrupt or future preference documents stop plugin startup with a visible error; they are never replaced with defaults or silently rewritten. Restore/repair the isolated file before restarting. Plugin packages, preferences and execution state are excluded from the existing cloud-sync and .nya backup contracts. Existing settings, redb tables, credential formats and user command data are unchanged.

Installation rejects symlinks and Windows reparse points, traversal, absolute resource paths, ADS/device aliases, archive links and duplicate case-insensitive paths. Limits are 512 entries, 64 MiB compressed archive, 128 MiB expanded package, and 32 MiB per file/component. Replacement reads components into memory before compiling; it holds no installed-file handles while executing. On Windows, a foreign process blocking directory replacement produces an error and preserves the old installation. Transaction commit failure restores the old directory. If even rollback fails due to external filesystem interference, the recovery tree is retained in staging and the error tells the user to preserve it.

Manifest schema 1

Plugin semantic version, manifest schema, and Wasm API version are distinct. The current schema is 1 and the API is exactly 1.0.0. Host compatibility uses semver requirements; prerelease builds must explicitly match a prerelease, for example >=2.0.0-preview.4. All unknown fields in schema-1 manifests, nested actions/parameters and preference documents are rejected. Arrays of authors, actions and parameters are extensible within their size limits; optional/default fields below may be omitted. There is no arbitrary extension/config JSON in V1. Future schemas must be explicitly implemented with compatibility fixtures.

schema_version = 1
id = "shell-templates"
name = "Shell templates"
version = "1.0.0"
authors = ["Your name"]
description = "Prepare commands for review"
host_version = ">=2.0.0-preview.4"
api_version = "1.0.0"
# component = "plugin.wasm" # Optional component-model binary
# capabilities = []        # Empty only; every extra capability is rejected

[[actions]]
id = "inspect"
name = "Inspect files"
description = "Draft a POSIX directory listing"
result = "command"        # command or text
template = "templates/inspect.txt" # Optional; otherwise implemented by component
quoting = "posix"         # literal (default), posix, powershell

[[actions.parameters]]
id = "path"
name = "Directory"
kind = "string"           # string, integer (signed 64-bit), boolean (true/false)
required = true           # false by default
default = "."             # Optional string parsed according to declared kind

IDs start with a lowercase ASCII letter and contain lowercase letters, digits, - or _, up to 96 bytes; Windows reserved names are forbidden. Contributions are addressed as plugin-id:action-id. Duplicate IDs cannot overwrite another plugin. Metadata name/author strings are limited to 160 bytes; descriptions to 4096 bytes; manifests to 128 KiB, actions to 128 and parameters per action to 32.

Resource paths are relative UTF-8 paths with / separators, up to 240 bytes. Both paths and every package entry follow the cross-platform safety rules. Template files are UTF-8 and support only ${declared-parameter} substitution. Unclosed/undeclared substitutions are rejected. There are no expressions, conditionals, includes or implicit script evaluation. posix single-quotes each substituted value and escapes single quotes; powershell doubles single quotes inside a single-quoted value. literal inserts the value verbatim into the draft. Quoting applies to parameters, not to the whole resource. Every result remains a user-reviewed draft, and the quoting mode must match its intended shell.

Rust SDK and WIT

nyaterm-plugin-api version 1.0.0 has no desktop, core, store, transport or GPUI dependency. Its published-package layout includes wit/plugin.wit; both guest wit-bindgen and host Wasmtime bindings are generated from this same file. A third party can use the SDK from a NyaTerm Git checkout:

[package]
name = "my-plugin"
version = "1.0.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
nyaterm-plugin-api = { git = "https://github.com/nyakang/nyaterm" }

Pin a reviewed Git revision when distributing a plugin. No SDK publication is performed by this change. For local development use a path to the SDK crate.

use nyaterm_plugin_api::{ActionInput, ActionResult, Plugin, PluginError};

#[derive(Default)]
struct SortLines;
impl Plugin for SortLines {
    fn invoke(&mut self, input: ActionInput) -> Result<ActionResult, PluginError> {
        let text = input.text.unwrap_or_default();
        let mut lines: Vec<_> = text.lines().collect();
        lines.sort();
        Ok(ActionResult::Text(lines.join("\n")))
    }
}
nyaterm_plugin_api::register_plugin!(SortLines);

WIT exports api-version, initialize(identity), invoke(action-input) and shutdown. Parameters use typed text/integer/boolean variants; results are either a command record (title, command) or text. Errors distinguish invalid input, unsupported action and plugin failure. The registration macro retains one mutable guest for serial calls; reload starts a fresh instance. Guest errors are presented as bounded generic host messages to avoid leaking user input from guest diagnostics.

The SDK embeds a nyaterm:plugin-api custom section. The host requires exactly one marker matching 1.0.0, verifies actual typed component exports using the generated bindings, and calls the exported version function. A forged manifest or marker cannot authorize a different interface. Only this ABI is implemented.

Build, debug and package

Install the Rust target once, then build examples and validation fixtures:

rustup target add wasm32-unknown-unknown
python scripts/plugins/build.py --fixtures
python scripts/plugins/verify.py

The build script obtains native-tool and guest target directories from Cargo metadata. It honors CARGO_TARGET_DIR and target-directory configuration. For example, in PowerShell, put build artifacts on a drive with sufficient space:

$env:CARGO_TARGET_DIR = 'D:\NyaTermBuild'
python scripts/plugins/build.py --fixtures
cargo build --locked

Installable examples still appear under the checkout's target/plugin-examples.

The guest examples have an isolated workspace at examples/plugins/guests with a checked Cargo.lock. This avoids nested-workspace conflicts and does not link the desktop into guests. The script builds core modules using the SDK, then uses wit-component to produce real components, validates their ABI/initialization, and writes installable packages under target/plugin-examples:

  1. templates: declarative ls draft with a quoted parameter.
  2. diagnostic-command: Rust/Wasm hostname diagnostics draft with per-instance invocation count; never performs DNS/network/process operations itself.
  3. text-tools: Rust/Wasm line sorting and JSON pretty-printing.

The SDK-built adversarial fixture is also reproducible. Checked binary fixtures under nyaterm-plugin-host/tests/fixtures allow host tests from a clean checkout without installing a guest target; rebuilding uses the same SDK and WIT.

For another guest:

cargo build --manifest-path /path/to/my-plugin/Cargo.toml --release --target wasm32-unknown-unknown
cargo run -p nyaterm-plugin-host --bin nyaterm-plugin-tool -- component /path/to/my_plugin.wasm /path/to/package/plugin.wasm
cargo run -p nyaterm-plugin-host --bin nyaterm-plugin-tool -- validate /path/to/package
cargo run -p nyaterm-plugin-host --bin nyaterm-plugin-tool -- pack /path/to/package /path/to/plugin.zip

Register the package directory for development, edit/build its resources, then reload explicitly. Validation and initialization errors preserve the previous instance. The manager shows diagnostics, never raw Wasmtime stacks or user text.

Isolation and limits

Wasmtime 39.0.1 (MSRV 1.89) runs Component Model guests on dedicated serial workers. wit-bindgen 0.49.0 has MSRV 1.82; the SDK declares 1.89. The tested toolchain is Rust 1.97.1 on Windows x64. Each instance owns its Store and mutable guest; different plugins have separate workers. The process manager accepts at most 16 queued management operations; each guest accepts 8 queued calls.

Initialization and calls have a 2-second deadline including queued wait, a 20-million fuel budget and epoch checks every 10 ms. Epoch callbacks trap on deadline/cancellation; they do not yield. Timeout actually stops execution. Memory is limited to 32 MiB per linear memory, at most 4 memories, 4 tables of 16,384 elements, 8 core instances, and 512 KiB Wasm stack. Inputs and outputs are limited to 256 KiB; command titles to 160 bytes. Results containing control characters other than ordinary tabs/newlines are rejected. Guest memory, trap, budget, deadline and invalid-result failures retire only that instance.

Cancellation, disabling, replacement and uninstall invalidate the old generation and retire its worker. Dropping a pending invocation cancels it. Shutdown joins all workers before AppShell quits, off the GPUI thread. Resource ownership is explicit: snapshots own temporary directories, the registry owns instances, instances own Stores/queues, and the engine owns its epoch clock. Components receive no inherited stdin/stdout/stderr or WASI environment. No guest logging import is exposed. CPU execution can be interrupted; compilation is bounded by package size and runs on the manager thread, rather than in a render/update path.

Verification and current limits

Run the complete repository checks with:

python scripts/plugins/verify.py --workspace

This builds the application and runtime helpers, checks the workspace, runs all tests, verifies formatting, and runs Clippy. Actual command results and any baseline failures are maintained in plugin-system-progress.md.

Windows x64 verification used a HEAD source snapshot with only plugin changes, and a fresh build cache: default native build, workspace check, all tests (3280 passed, 13 existing ignored), formatting and Clippy all passed. No new plugin tests are ignored. The SDK's packaged WIT was independently compiled; the host also passed ARM64 Windows cross-compilation, without ARM64 runtime execution.

The current workspace, including subsequent terminal changes and the explicit backup/sync isolation regression, also passed check, formatting and all-targets Clippy. Serial workspace tests passed 3290 with 13 existing ignored tests. Default-parallel reruns encountered an unchanged ConPTY close timeout and an unchanged HTTP test socket timeout; each exact rerun passed. Their failures and the serial result remain recorded in the progress document. The detailed acceptance table separates automated evidence from the outstanding native walkthrough.

Windows x64 host tests exercise real SDK component initialization/calls, state isolation, archive/path safety, junction rejection, file occupation, transactional rollback, enable persistence, cancellation, stale-result rejection, resource failures and shutdown handle release. GPUI tests cover shared process ownership and composer-only adaptation. Native UI automation is currently unavailable because the computer-use native pipe cannot connect, so manual visual/keyboard interaction is not claimed as verified. macOS/Linux and Windows ARM64 execution are not claimed as tested. Native platform acceptance remains an explicit check.

V1 has no marketplace, automatic updates, plugin sync/backup, arbitrary UI, protocol decoders, KV or host capabilities. Add a capability only with a narrow WIT import, host enforcement of declaration ∩ user grant ∩ supported operation, revocation cancellation and compatibility tests. Extend schema/API versions explicitly; never infer ABI compatibility from the manifest alone.