Status: Active · Schema version: 1 · Implementation: plugins/ (vibeshell-plugins crate)
This document is the normative specification for VibeShell plugins. The development guide (plugin-development.md) is the companion how-to; when the two disagree, this document wins. The words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119.
A VibeShell plugin is a declarative JSON manifest. It describes actions — a static program plus arguments — that VibeShell renders and executes against a connected session. No third-party code (JavaScript, HTML, WASM, native libraries) ever loads into the privileged app process. The host owns validation, rendering, quoting, execution, and output capture.
Three roles interact with this spec:
| Role | Responsibility |
|---|---|
| Author | Writes a conforming manifest. |
| Host | A VibeShell build that validates, installs, renders, and executes plugins. |
| Distributor | Moves manifest files between devices (file share, repo, backup file). |
- A plugin is distributed as a single JSON document, UTF-8 encoded, at most
256 KB (
MAX_MANIFEST_BYTES). - The document is the manifest described in §4. Export (§8) produces exactly this document; there is no wrapper, envelope, or signature in v1.
- The recommended file name is
<id>-<version>.plugin.json. Hosts MUST accept any.jsonfile whose contents parse as a manifest. - v1 deliberately ships no remote registry, no package signing, and no update channel. Any future registry MUST add signature verification before it may distribute manifests.
Every plugin has an id that is globally stable across devices and backups:
- 3–64 characters; ASCII lowercase letters, digits,
.,_,-; MUST start with a lowercase letter or digit (the same grammar ascategory,icon, action ids, and input ids). - The host namespaces installed state by
id. Two manifests with the sameidare the same plugin; installing a newer version updates it (§7). - External plugins MUST NOT use an
idthat collides with a built-in plugin; hosts MUST reject such imports.
{
"schemaVersion": 1,
"id": "acme.service-tools",
"name": "Service Tools",
"description": "Inspect an Acme service on the connected server.",
"version": "1.0.0",
"author": "Acme",
"category": "operations",
"icon": "wrench",
"permissions": ["remote_exec"],
"sessionTypes": ["ssh"],
"defaultSettings": {},
"entry": {
"type": "commands",
"actions": [ /* §4.2 */ ]
}
}| Field | Required | Rules |
|---|---|---|
schemaVersion |
yes | MUST be 1. |
id |
yes | §3 grammar. |
name |
yes | ≤ 80 bytes, non-empty. |
description |
yes | ≤ 600 bytes, non-empty. |
version |
yes | ≤ 32 bytes. Free-form; semver recommended. |
author |
yes | ≤ 80 bytes. |
category |
yes | Identifier grammar; used for marketplace grouping. |
icon |
yes | Identifier grammar; a Lucide icon name known to the host. Unknown names fall back to the plug icon. |
permissions |
yes | Non-empty list from §5.1, no duplicates. |
sessionTypes |
yes | Non-empty subset of ssh, local. |
defaultSettings |
no | JSON object; ≤ 16 KB serialized; MUST NOT contain secrets. |
entry |
yes | §4.2 (commands) or §4.4 (native). |
Hosts MUST ignore unrecognized fields so that forward-compatible additions
remain loadable by older builds. A host that rejects a schemaVersion it does
not support MUST report the highest version it supports.
An entry of type commands declares 1–24 actions:
{
"id": "container-logs",
"name": "Container logs",
"description": "Read recent logs.",
"program": "docker",
"args": ["logs", "--tail", "200", "{{input.container}}"],
"inputs": [ /* §4.3 */ ],
"requiresConfirmation": false,
"elevate": false,
"allowSudo": false,
"output": { "kind": "text" }
}| Field | Required | Rules |
|---|---|---|
id |
yes | Identifier grammar; unique within the plugin. |
name / description |
yes | ≤ 80 / ≤ 300 bytes. |
program |
yes | ≤ 256 bytes; ASCII alphanumerics plus _ - . / +; no ... A binary name or absolute path. |
args |
no | ≤ 32 arguments, each ≤ 2048 bytes. |
inputs |
no | ≤ 8 inputs (§4.3). |
requiresConfirmation |
no | Default false. The host MUST prompt before every run when true. MUST be true when elevate is true. |
elevate |
no | Default false. Runs under sudo (built-in plugins only in practice; see §5.2). |
allowSudo |
no | Default false. Offers an explicit "try sudo" path without elevating by default. Mutually exclusive with elevate. |
output |
no | Default text (§4.5). |
Templates. An argument may be exactly {{input.<id>}} and nothing else.
Embedded templates (prefix={{input.x}}) MUST be rejected. Every referenced
input MUST be declared on the same action. Rendering substitutes the raw value
for the whole argument, then the host POSIX-shell-quotes the argument
(single-quote wrapping with '"'"' escaping; an empty value renders as '').
The frontend cannot submit a replacement command — it sends input values,
never commands.
The quoting guarantee protects the outer command's argument structure. It
is not a sandbox: the invoked program may itself interpret an argument as SQL,
a glob, or shell source. The manifest author owns that semantic boundary and
SHOULD set requiresConfirmation on actions that execute user-provided code
or mutate state.
| Field | Required | Rules |
|---|---|---|
id |
yes | Identifier grammar; unique within the action. |
label |
yes | ≤ 80 bytes. |
description / placeholder |
no | ≤ 240 / ≤ 120 bytes. |
required |
no | Default false. |
kind |
no | One of text (default), integer, boolean, select. |
options |
select only | 1–32 unique options, each ≤ 80 bytes. |
Values are limited to 1,024 bytes and MUST NOT contain NUL, LF, or CR. An
omitted optional input renders as an empty string argument (useful as sh -c
script positional arguments). Select inputs MUST submit one of the declared
options.
entry may be { "type": "native", "view": "..." }. Native views are
compiled React components shipped inside the app; the only defined view is
server-status. External manifests MUST NOT declare native entries — hosts
MUST reject them.
"output": { "kind": "table", "columns": ["ID", "Name"], "delimiter": "\t" }kind:text(default) ortable.- Tables declare 1–16
columnsand adelimiterof 1–4 bytes (no newlines). The command SHOULD print one row per line; the host splits on the delimiter and pads/truncates cells to the column count, rendering at most 1,000 rows. - Captured output is bounded to ~1 MB with UTF-8-boundary-preserving truncation (§6).
| Permission | Grants | Who may declare it |
|---|---|---|
remote_exec |
Run action commands on connected SSH sessions. | Anyone. Required for plugins supporting ssh. |
local_exec |
Run action commands in local shell sessions. | Anyone. Required for plugins supporting local. |
local_system_read |
Read host system metrics (native views). | Built-in only. |
- Importing a manifest MUST NOT enable it. External plugins install disabled with an empty grant set; enabling requires explicit user confirmation and grants exactly the manifest's declared permissions.
- Disabling a plugin MUST revoke its stored grants.
- Built-in plugins are reviewed at build time and MAY auto-grant on install.
- The host resolves the installed manifest and action server-side before executing anything; the renderer cannot synthesize commands.
- Input values are normalized, size-checked, and quoted as single arguments (§4.2).
- Elevated execution uses
sudo -S -p ''with the password written to stdin (never on a command line), orsudo -nfor NOPASSWD hosts when no password is supplied. Passwords live in memory only. - Manifests, settings, and inputs MUST NOT contain secrets. Database actions
SHOULD rely on authentication already configured on the remote host
(
.pgpass, Unix sockets,docker execinto the service container, …). - Third-party UI code never runs in the app's webview.
remote_exec remains a powerful capability: the author picks the program and
fixed arguments. Users SHOULD only import manifests from sources they trust.
The host renders program + args (with quoted inputs), then:
- SSH sessions: executes through the existing SSH session. The transport
bounds execution time and output capture; the plugin result is then limited
to
MAX_PLUGIN_OUTPUT_BYTES(1 MB) with UTF-8-safe truncation. The host MUST NOT pipe throughheadto obtain this limit, because that masks exit status. - Local sessions: spawns
$SHELL -c <cmd>with a 60-second timeout. Stdout and stderr are concurrently drained into bounded buffers; unused stdin is closed so programs waiting for EOF cannot hang indefinitely. - Successful results carry
pluginId,actionId,output,durationMs, andtruncated. A non-zero exit status is an error, not a successful result. This API does not expose a separate numeric exit-code field. - GUI, native CLI and MCP use the same installed/enabled state, permission checks, validated inputs and execution implementation. An operation records its rendered command and lifecycle in the encrypted local activity store; passwords supplied through protected stdin are not recorded.
requiresConfirmation, elevation, optional sudo and command-risk policy MUST be checked by the backend. Native CLI callers explicitly provide--confirmonly after human consent. MCP MUST obtain human approval through the gateway; a model-suppliedconfirmedvalue is not approval. A command that changes while approval is pending MUST require a new review.
| Transition | Semantics |
|---|---|
| Import | File picked by the user, size-checked (≤ 256 KB), parsed, validated with the external policy, stored. Result: installed, disabled, empty grants. |
| Install (built-in) | Copied from the compiled-in catalog. Result: installed, enabled, granted the manifest's permissions. |
| Enable | User confirms; grants ← manifest permissions. |
| Disable | Grants ← ∅. |
| Update | Importing a manifest whose id matches an installed plugin: non-sensitive settings are preserved, the manifest is replaced, the plugin is disabled, and grants are revoked. The user reviews and re-enables. |
| Uninstall | Installation record deleted. The built-in catalog entry (if any) remains visible as uninstalled. |
| Settings | Per-plugin JSON object, ≤ 16 KB, updated at runtime; included in backups (§9). |
- Import takes any conforming manifest file (§2) and follows §7 Import.
- Export writes the manifest as pretty-printed JSON with a trailing
newline, named
<id>-<version>.plugin.json:- Installed external plugin → the manifest it was imported with.
- Built-in plugin (installed or not) → the shipped manifest, doubling as an
authoring template. Note that exporting a native-view built-in (e.g.
server-performance) produces a document that cannot be re-imported as external (§4.4); it is useful for reference and templates.
- Settings are never exported. They are device state and travel only via backups (§9).
Plugin installations are a first-class sync entity (plugin_installation)
alongside servers, groups, and command snippets. Consequently every backup
and sync mechanism built on the sync pipeline — the encrypted portable backup
file and cloud sync providers — carries plugins automatically.
Payload (camelCase on the wire):
{
"pluginId": "example.remote-tools",
"version": "1.0.0",
"source": "external",
"enabled": true,
"manifestJson": "{ ...完整 manifest(仅 external)... }",
"settingsJson": "{\"rows\":25}",
"installedAt": 1730000000,
"updatedAt": 1730000900
}Rules:
- External plugins carry their full
manifestJson; built-in plugins set it tonullbecause their manifest resolves from the local catalog. - Granted permissions are never transported. The receiving device recomputes them: enabled → exactly the manifest's declared permissions; disabled → none.
- A restored plugin that is unknown on the receiving device (e.g. a built-in plugin from a newer app version, or an external manifest that no longer validates) restores disabled with empty grants rather than failing the backup import.
- Deleting a plugin installation emits a sync tombstone like any other entity; portable backup files intentionally omit tombstones and merge rather than delete.
Built-in plugins live in the plugins/ workspace crate, one directory per
plugin:
plugins/
├── Cargo.toml # vibeshell-plugins
├── src/lib.rs # spec implementation: types, validation, rendering, catalog
└── builtin/
├── server-performance/plugin.json
├── docker-containers/plugin.json
├── redis-inspector/plugin.json
└── ... # 12 built-ins in this catalog
Authoring rules for built-ins:
- Add
plugins/builtin/<id>/plugin.jsonand register the pair inBUILTIN_MANIFESTS(src/lib.rs). The directory-id ↔ registration-id test fails the build otherwise. - Built-ins are validated with the builtin policy: they may additionally
declare
nativeentries andlocal_system_read. - Every built-in manifest MUST satisfy the same v1 schema as external ones otherwise — built-ins get no schema exceptions.
- The catalog is embedded at compile time (
include_str!); shipping a new or updated built-in requires an app release.
Every supported plugin, including imported manifests and native performance reads, MUST expose action input schemas and a current usage reference. Installation state and documentation are data, not instructions that can expand an Agent's authority.
vibeshell plugins list --installed --json
vibeshell plugins describe <plugin-id>
vibeshell plugins docs <plugin-id>
vibeshell plugins run <plugin-id> <action-id> --session <session-id> --inputs '{}'list reports actual installed/enabled state, permissions, session types and
reference commands. Without --installed it also includes uninstalled
catalog entries. describe returns JSON action schemas with required inputs,
input types, confirmation and sudo capabilities. docs returns Markdown
regenerated from the current validated manifest. Neither returns runtime
settings, saved credentials or sudo passwords.
run reuses the selected session and never installs or enables a plugin.
--inputs MUST be a JSON object conforming to the selected action's schema;
--confirm and --sudo are explicit user-authorized options, not defaults.
Local sessions require a GUI-owned service that can access that local session.
Remote native performance collection currently assumes Linux /proc.
MCP exposes the same operations as plugin_list, plugin_describe and
plugin_execute. plugin_describe with reference: true returns the full
reference; plugin_execute uses camelCase pluginId, actionId, sessionId
and inputs. Clients MUST discover schemas rather than guessing action IDs.
The main VibeShell Skill MUST contain discovery instructions and a compact
index of built-in reference documents, but MUST NOT inline every action's
usage. The native Skill installer generates references/<plugin-id>.md from
all built-in manifests before publishing SKILL.md. Imported or updated
plugin references are obtained live through plugins docs; static references
are not authoritative for the current installation. The installer test checks
catalog/index/reference/action parity and repeated-install consistency.
schemaVersiontracks the manifest format. v1 is the current version. A future v2 may add fields (older hosts ignore unknown fields, §4.1) or change semantics (requires a bump).- Hosts MUST reject manifests whose
schemaVersionthey do not understand. - The wire format of sync payloads (§9) is versioned separately by the sync envelope and is not part of manifest compatibility.
- Plugin
versionis opaque to the host; it is display and export naming only.