Files
nyaterm/docs/plugin-system.md
T
Kang 2250e90dba feat(plugin-sdk): add new job for building and testing plugin SDK components
Introduced a new job in the GitHub Actions workflow for the Plugin SDK, which includes steps for checking out the repository, setting up Rust with necessary components, installing Linux dependencies, checking formatting, building SDK guests, and verifying the packaged SDK. Updated the Rust gate job dependencies to include the new plugin-sdk job.

Additionally, refactored the NyaTermApp to improve the handling of floating panel close actions based on the current panel state, ensuring better user experience. Enhanced plugin management by implementing shutdown procedures and revising invocation checks to include expected revisions, improving stability and error handling during plugin operations.

Added comprehensive tests for the plugin service to validate the new functionality and ensure robust performance across various scenarios.
2026-10-04 20:28:18 +08:00

349 lines
18 KiB
Markdown

# 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 **Plugins** using the extension icon in the bottom-left activity bar,
between **Sync / Backup** and **Settings** by default. The side panel follows the
workspace's docked/floating mode and can be moved or hidden like other panels.
Click its activity-bar icon again to hide it; the plugin panel has no top-right
close button.
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**.
**Cancel** applies only to action calls. Cancelling an executing guest retires its
instance; reload it before invoking it again. An abandoned queued call does not
execute. Installation, update, enable/disable, reload and uninstall run to
completion or roll back and cannot be cancelled from the panel. Hiding the panel
retains its state; closing a workspace cancels its action call while accepted
management continues in the shared process service.
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:
```sh
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.
```toml
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:
```toml
[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.
```rust
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:
```sh
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:
```powershell
$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:
```sh
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.
Orderly application exit rejects new and queued operations, finishes any started
management transaction, then joins plugin workers and the epoch clock. Execution
timeouts do not cover Wasmtime compilation or filesystem I/O. These remain
in-process background work, so shutdown may wait for them; V1 does not promise a
hard compilation timeout. Selected revisions are checked before guest dispatch,
preventing stale form inputs from reaching replacement instances.
## Verification and current limits
Run the complete repository checks with:
```sh
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`.
The script also checks guest formatting, rebuilds SDK components, verifies the
packaged SDK and checks architecture boundaries. Each invocation owns a fresh
`target/plugin-verification/run-<id>/` directory, preserving previous failures.
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](plugin-system-acceptance.md) separates automated evidence from
the current Windows native walkthrough and historical results.
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. The lifecycle closeout passed SDK source builds,
SDK packaging, plugin regressions and the default-parallel workspace suite
(3305 passed, 13 existing ignored), build/check/fmt/Clippy and workflow syntax.
The architecture gate still reports the existing transfer path-bar violation;
plugin crate boundaries pass and no allowlist was expanded.
The recovered native computer-use connection exercised all three examples in
an isolated Windows x64 portable instance: result editing and explicit draft
append/replace, text copy/reuse, shared-window enable/disable/uninstall, failed
update/development reload, guest fault isolation, restart preferences and normal
exit with released database/Wasm handles. No command was sent and real user data
was untouched. Precise lifecycle races use automated barriers. macOS/Linux and
Windows ARM64 execution are not claimed as tested. The
[acceptance record](plugin-system-acceptance.md) lists the evidence and limits.
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.