A declarative plugin describes language support, build and run integration, version control,
debugging, project generation and host-rendered UI as data. The host reads
plugin.json, validates it, checks the applicable signature or community approval, and launches the
declared external processes itself. The host never loads plugin code into its
own process, so a package does not depend on the host's binary layout and does
not need to be rebuilt for each Lithe release.
This directory is the cross-platform source of truth:
| Path | Purpose |
|---|---|
plugin-manifest.schema.json |
JSON shape of plugin.json |
native-ui.schema.json |
JSON shape of native UI documents rendered by the host |
plugin-index.schema.json |
JSON shape of a marketplace source index |
plugin-l10n.schema.json |
JSON shape of a plugin's translation file l10n/<language>.json |
fixtures/ |
Valid example manifests and golden cases, one folder per contract area (below). Manifests are named after the capability they exercise rather than a published plugin. Case files name their base manifest by file name, so file names are unique across the folders |
toolchains/README.md |
Implemented Core foundation: plugin definitions/provider declarations, registry (a short tool ID defined by several enabled plugins belongs to none of them), reply validation, directory pins, snapshot resolution, version matching and discovery plans; hosts that declare toolDefinitions use it for read-only settings pages and for Build and run tools |
| Fixture folder | Contents |
|---|---|
fixtures/manifest/ |
hello-lithe.plugin.json, the smallest UI plugin, and invalid-manifests.json: edits to a valid manifest and the rejection code every host must report |
fixtures/languages/ |
Language support: packaged, system-command, and host-integration language servers, node-runtime for a runtime launch, and language-grammar with grammars/; command-plans.json holds golden run/test command expansions and language-server launch plans; language-server-backend-launch.plugin.json is a language server whose backend resolves the launch and a session policy, and language-server-launch-resolution.json holds its golden first-step plans and decoded answers |
fixtures/backend/ |
Plugin backends (backend, backend-workspace, command-backend, terminal-backend); cross-plugin calls (plugin-functions-exporter, plugin-functions-caller); function-protocol.json holds golden plugin-function negotiation, registration, invocation, outcome, progress, and refusal cases, plugin-logging.json the golden litheLogging negotiation and notifications |
fixtures/editor/ |
Editor actions served by a backend: editor-actions.plugin.json, icons, alternatives and click results in editor-actions.json; editor providers in editor-providers.plugin.json with registration and result cases in editor-providers.json |
fixtures/ui/ |
Native controls, forms, and panels, and a web panel |
fixtures/settings/ |
Plugin settings; l10n/zh-Hans.json translates settings.plugin.json, and localization-cases.json holds golden translation choices and rejections |
fixtures/build-systems/ |
Build systems (build-*: static file, command output, instance options, model producer and consumer, tool path, toolchains); build-plans.json holds golden build-system plans and decoded outputs |
fixtures/build-providers/ |
Build providers (build-provider*) and the golden cases of their configuration, run options, run and debug preparation, execution, and test protocols |
fixtures/debug-adapters/ |
Debug adapters (standalone, served by a language server, remote attach, and debug-adapter-features with declared features and defaults), debug-adapter-rejections.json, and debug-start.json: what debug.decodeBackendConfiguration returns for each debug/start case |
fixtures/run/ |
Run sessions and console links: tool-reference-launch names its runtimes by tool-definition ID and declares output link matchers; run-start.json holds what run.decodeStart returns for each case, including the rejections, and output-link-matchers.json the golden matches and resolutions |
fixtures/vcs/ |
A version control provider |
fixtures/keymap-import/ |
Two independent shortcut import providers sharing one packaged backend |
fixtures/project-generators/ |
Project generators: template files (templates/), a scaffolding command, and an archive download; project-generator-tool-field.plugin.json with templates/tool-field/ picks tool installations with tool fields; project-plans.json holds golden project.* form evaluations, generation plans, and name checks, and a case's optional manifest names the fixture it runs against (project-generator.plugin.json by default) |
fixtures/toolchains/ |
Production-shape fixtures for tool definitions and a packaged toolchain provider; they require toolDefinitions and toolchainProviders, which a host advertises only when it implements them |
fixtures/package-trust/ |
publisher-bundled.plugin.json and signature-vectors.json: public cross-platform vectors for the package publisher signature: the RFC 8032 test key, a fixed package with non-ASCII file names, its canonical payload, RFC 8032 deterministic signature, and content digest. Both platforms must build the same canonical payload and digest and accept the recorded signature; signers that add randomness, such as CryptoKit, check the signature by verification instead of byte comparison |
fixtures/marketplace/ |
Marketplace index, availability, source URL, and install review golden cases |
Validation and template expansion are implemented once in Rust Core
(rust/crates/plugin-manifest/ and the rust/crates/plugin-* crates beside
it, with build systems in rust/crates/build/src/) and exposed through the
plugins.declarative.* commands in rust/contracts/rust-core-api.md. Hosts must not
reimplement them.
This directory is the only copy of the contract. It lives in shared/plugins/
of the Rust Core repository together with the
developer guide and the official SDKs, and changes in the
same pull request as the Core code that enforces it. Plugin authors read it
here and never need the Lithe application repository. Each schema's $id is
its published URL,
https://raw.githubusercontent.com/Lithe-IDEA/Lithe-core-rust/main/shared/plugins/contract/v1/<file>;
node shared/plugins/scripts/verify-docs.mjs checks the IDs and the relative
links. plugin.json may not carry a $schema key, because unknown fields are
rejected; editors associate the schema by file name instead.
All tool definitions belong to enabled plugins; the host supplies only generic query, validation, resolution, persistence, and presentation mechanisms. It does not add a built-in tool-definition table or interpret version-manager configuration formats.
The implemented Core contract accepts toolDefinitions: 0
and toolchainProviders: 0 declarations and provides the toolchains.* commands
listed there. Tool-definition candidates follow
Executable resolution and do not admit new sources into
existing Build candidate sets. Definitions are entirely
plugin-owned. Any plugin may define a short ID such as compiler or a qualified
<pluginID>:<id>; the host supplies no provenance evidence. A short ID defined
by more than one enabled plugin belongs to none of them: the registry omits
those definitions and reports the contest as a conflict.
A host that advertises these capabilities uses them for its read-only toolchain settings pages.
The native adapter scans declarations, probes checked executables, queries plugin
backends and watches their inputs; Core consumes captured facts and owns validation
and resolution. A separate fresh resolution serves Build tools and run plans
whose toolchain executable names a plugin-defined tool rather than a host
toolchain; settings detections never grant launch authority. Saving pins,
toolchainConsumers, generic runtime launches and live backend activation remain
unsupported, and language-server consumers keep their previous adapters. Version-manager
implementations such as mise belong to their own plugin packages. See
the implemented slice.
keymapImportProviders contains one to eight {id, displayName, recognition, inputs} entries.
IDs are unique within the plugin, start with a lowercase letter, contain only
lowercase ASCII letters, digits and hyphens, and are at most 64 bytes. The
plugin requires keymapImportProviders: 0, backend: 0,
backend.launch, keymaps.provide and keymaps.readSelectedSources. Its backend starts from a packaged
executable or packaged runtime without discovered runtime dependencies. The
host starts it for an explicit import; it is not a startup or workspace-open
activation event. The permission permits offering mappings; it grants no file
access on its own. keymaps.readSelectedSources authorizes only the provider's
declared inputs under the source directory the user explicitly selected for this
import, and only for the lifetime of that import.
recognition lists one to sixteen distinct metadata-only probes. A
{kind: "file" | "directory", path} probe matches an entry of that kind
beneath the selected root, using the input path restrictions and refusing
symlinks. A {kind: "root", base: "userConfig", components} probe matches
the selected directory's exact path beneath the platform's user configuration
directory (macOS Application Support, Windows roaming AppData). Components
contain one to eight safe literal names and may end in one *, matching
exactly one directory component; the first component must be literal. The
source matches any probe. Recognition never authorizes reading file contents
or discloses the absolute path to the provider. A host may use probes without
launching backends, and must recheck them on import.
inputs lists one to sixteen unique selectors. {kind: "file", path} selects
one relative file; {kind: "directory", path, extension} selects immediate
children of that directory with the exact lowercase alphanumeric extension,
never recursive descendants. Paths have at most 256 characters, contain no
empty, . or .. segments, backslashes, colons, wildcards, control characters,
or segments ending in a dot or space. Directory selectors have at most 64
matches after scanning at most 4096 directory entries. The native reader must reject symlinks and paths escaping the
selected root, bound each file to 1 MiB and the snapshot to 8 MiB, and expose
only scoped opaque handles in chunks of at most 128 KiB. A manifest cannot
authorize arbitrary paths or access an unselected source.
Core offers experimental.litheKeymapImport: {versions: [0], providers} only
to a plugin-owned session with declared providers. The backend selects a
nonempty duplicate-free subset as {version: 0, providers}. A request to an
unselected provider is refused. lithe/keymap/import carries only
{providerID, snapshotID}. The snapshot is an opaque host-owned reference;
the provider never interprets it as a path. Core bounds the operation and
validates normalized results against the host's action catalog. Providers own
source formats and must not silently promote conditional or multi-stroke rules
to global shortcuts. The exact Core commands, limits and fixtures are in the
Rust Core API.
When granted individually, methods lithe/host/keymapImport/listInputs
and lithe/host/keymapImport/readChunk take snapshotID and providerID.
Listing returns {files: [{id, path, byteCount}]}; a read additionally takes
fileID, a nonnegative offset and maximumBytes from 1 through 131072 and
returns {data} containing base64 bytes. Paths are relative to the selected
root and file IDs are opaque. The host binds snapshots to their plugin, session
and provider, revokes them when the import or session ends, and rejects files
replaced or changed after capture. A new snapshot for that provider revokes the
previous one. Errors never disclose absolute paths.
A host that advertises keymapImportProviders 0 implements the scoped reader.
A host advertises the capability only with the complete import workflow. The official Node,
Java and Rust SDKs implement litheKeymapImport 0.
<plugin-id>/
├── plugin.json # this contract
├── lithe-plugin-signature.json # publisher signature over every other file
├── l10n/<language>.json # optional translations (see Translations)
└── … # resources referenced by plugin.json
Every path in plugin.json is relative to the package root, uses /, and may
not contain .., a leading /, a backslash, or NUL. Packages may not contain
symbolic links. A host treats an installed package as read-only.
| Field | Meaning |
|---|---|
schemaVersion |
Always 0. Any other value is refused with plugin.schema.unsupported. |
id |
Stable plugin ID: lowercase letters, digits, . and -. |
displayName |
Name shown in plugin management. |
version |
Plugin version, major.minor.patch. Independent of the Lithe version. |
pluginAPIVersion |
Always 0. Any other value is refused with plugin.api.unsupported and field: "pluginAPIVersion". Lithe's own version is never used for compatibility. See Versioning. |
requires |
Required: every capability the manifest uses, each with version 0; see Capabilities. |
requiresTools |
Every tool-definition ID the manifest's launches and runtime dependencies name; see Tool references. |
vendor |
id, displayName, and signatureRequirement (see Trust). |
permissions |
Capabilities the plugin may ask the host to perform. |
activationEvents |
When the host may start each provider. |
languages |
Languages the plugin recognizes. |
languageServers |
At most one language server per declared language. |
executionProviders |
At most one run/test provider per declared language. |
settings |
User-adjustable plugin options. |
ui |
Host-rendered tool windows and commands. |
buildSystems |
Project-level build tools. |
toolDefinitions |
Plugin-owned tool policy (Core foundation); requires toolDefinitions: 0; a host may offer read-only discovery and resolve Build and run tools. |
toolchainProviders |
Backend fact-provider declarations (Core foundation); requires toolchainProviders: 0, backend: 0 and toolchains.provide. |
keymapImportProviders |
Independent shortcut import providers; requires keymapImportProviders: 0, backend: 0 and keymaps.provide. A host provides access to explicitly selected sources. |
pluginFunctions |
Functions the plugin exports to, and calls from, other plugins; requires pluginFunctions: 0 and backend: 0, and grants no permission. A host that advertises this capability relays the calls. |
Unknown fields are refused (plugin.manifest.invalid) so a host rejects a
package instead of ignoring part of it. A new optional field in an existing
section needs no new version; a change to what a section means gives the one
capability that owns it version 1, and only plugins that use that change
declare it (see Versioning).
id is lowercase ([a-z0-9+_-]). fileExtensions, fileNames, and
projectFileNames are matched case-insensitively; at least one must be
present. A leading dot is optional only in fileExtensions (.php equals
php). In complete file names, project markers, and management file names it
is part of the name, so .toolrc and .env.example stay hidden-file names. Optional dependencies lists managementFileNames,
projectDependencyPaths, and projectBinaryPaths relative to the project.
A language has two optional fields, grammar and configuration. Both are data: the host
reads them from the package and the editor applies them without starting any
process, so a language with a grammar needs no language server, backend, or
activation event, and a plugin may consist of grammars alone.
{
"id": "http",
"displayName": "HTTP Request",
"fileExtensions": ["http", "rest"],
"grammar": { "kind": "textmate", "path": "grammars/http.tmLanguage.json" },
"configuration": {
"lineComment": "#",
"blockComment": ["/*", "*/"],
"brackets": [["{", "}"], ["[", "]"]],
"autoClosingPairs": [["{", "}"], ["\"", "\""]]
}
}grammar.kindnames the highlighting engine. The contract defines onlytextmate; another engine is added as a new kind, so a plugin can move between engines without changing anything else.- This is a static package contribution:
languages[]belongs to the manifest andgrammar.pathreferences a package JSON file. No public host method accepts inline grammar JSON to dynamically register, replace, or dispose a highlighting provider. Live installation/update/withdrawal of eligible static packages does not imply such an API or automatic reload after arbitrary file changes; installed packages remain read-only. - Adding another language or grammar using the supported structure needs only
plugin data; changing its rules needs no contract change. A new
engine or capability kind still needs an implemented host adapter and an
explicit contract; an arbitrary new
kindstring cannot supply one. - This contribution does not register completion, hover, definition, symbol,
formatting, rename, or Code Action providers backed by ordinary plugin
functions. A backend binds the provider types a host routes with
editor/registerProviders; existing LSP support andeditor/registerActionsare separate capabilities. grammar.pathis a package-relative file that stays inside the package. Hosts read it read-only from the installed package, pass its text toplugins.declarative.decodeGrammar, and register the returnedsource, the text Core validated byte for byte, under the returnedscopeName. A grammar Core refuses isplugin.grammar.invalidwith the failing field (such asgrammar.repository.string.patterns[2].begin); the language then opens as plain text and the rest of the plugin keeps working.- A TextMate grammar is the JSON form VS Code reads, a
.tmLanguage.jsonfile; plist and YAML grammars are converted to JSON first. The fixturefixtures/languages/grammars/http.tmLanguage.jsonis a complete example. A grammar is an object withscopeName, 1 to 128 bytes of ASCII letters, digits,.,-,_, and+, and apatternsarray, and optionallyrepositoryandname. Every rule is an object. The editor readsinclude,match,begin,end,while,name,contentName,captures,beginCaptures,endCaptures,whileCaptures,patterns,repository, andapplyEndPatternLast(true,false,0, or1), and Core refuses a grammar where one of them has another type. Captures are an object keyed by group number or an array by position; object keys that are not group numbers are ignored. Every other key, such ascomment,fileTypes,firstLineMatch, oruuid, is ignored and not checked. - Core accepts at most 2 MiB, 16384 rules counting every pattern, repository
entry, and capture, rules nested 32 deep, regexes of 32 KiB, capture group
4096,
nameandcontentNameof 1024 bytes, andincludetargets and repository keys of 256 bytes. VS Code's own grammars, the largest of which is C++ at 662 KiB and 3165 rules, are well within these bounds. - Regexes use the Oniguruma syntax TextMate grammars are written in. Core does not compile them: a pattern the editor's engine cannot compile never matches, and the rest of the grammar still applies.
includeis$self,$base,#namefor a repository entry, orscopeorscope#namefor another grammar. Another grammar resolves against those the editor has, its own and those of enabled plugins, which is how a grammar embeds a language such as CSS inside HTML; an include that resolves to nothing matches nothing, as in VS Code.injectionsandinjectionSelectorare not supported, so they are ignored and not checked.- A plugin grammar replaces the editor's own grammar of the same
scopeNamewhile the plugin is enabled. Two plugin languages cannot register the same scope: the first keeps it, and the other is reported and opens as plain text. configurationsetslineComment,blockComment(open and close),brackets, andautoClosingPairs. Each delimiter is 1 to 16 bytes without control characters; each list has at most 32 pairs.- Plugin grammars never run on the editor UI thread. The host bounds the backtracking of each regex search and the time one line may take, so a grammar cannot stall editing; a line not yet tokenized, or that the grammar could not tokenize within that budget, is drawn as plain text.
| Field | Meaning |
|---|---|
languageID |
A declared language. |
languageIdentifier |
LSP languageId for documents; defaults to languageID. |
launch |
{"kind": "packaged", "path": …} runs an executable inside the package. {"kind": "systemCommand", "commands": [...]} lists bare command names the host resolves with its tool discovery (PATH, project toolchains, user overrides). {"kind": "runtime", …} starts a package file with a packaged or discovered runtime; see Runtime launch. {"kind": "backend", …} has the plugin backend answer the command line at each start; see Backend-resolved launch. The launch fields are described in Process start. |
arguments |
Arguments passed to the server, written as launch templates. |
validationArguments |
Probe that must exit with status 0 before a candidate is used; empty means the host trusts the candidate and lets the LSP handshake decide. |
runtimeDependencies |
Tools the host must discover and pass to the server, each a tool-definition ID that requiresTools lists (see Tool references). The host exports each path as LITHE_<TOOL>_PATH (LITHE_NODE_PATH, LITHE_GO_PATH; a user-set value takes precedence) and prepends its directory to the server's PATH, because servers such as gopls run the tool themselves and an app opened from the Dock does not inherit the shell PATH. A project-local copy is never chosen, since servers start automatically when a file opens. |
initializationOptions |
A JSON object sent as the LSP initializationOptions. Every string value inside it is a launch template. |
hostIntegration |
Reserved. Core implements no host integration, so any ID is refused; see Language server host integrations. |
requestMethods, commands, serializedCommands, forwardNotifications, state, remote |
What the plugin backend may ask of the running server and how the host keeps its state; see Plugin-owned sessions. |
One enabled plugin serves a language. Enabling a plugin, or confirming the installation of an enabled one, turns off every other enabled plugin whose newest installed version declares a language server for one of the same languages; the host tells the user which plugin it turned off and why. Two plugins can serve the same language as alternatives, for example a lightweight navigation server and a complete one, but never at the same time, so neither has to know the other.
The host owns the language-server process lifecycle, the LSP session, and
stopping the process when the provider is disabled. A build provider with its own
process is the same kind of process: it declares launch, arguments,
validationArguments, runtimeDependencies, and initializationOptions with
the same meaning and validation; see Build providers.
hostIntegration is reserved. Core implements no host integration and never
matches on a language ID, a server ID or a tool name: everything a language
needs beyond its launch comes from the plugin's own backend, providers and
declarations. Any hostIntegration ID is refused with
plugin.integration.unsupported, at languageServers[<n>].hostIntegration.id,
so a manifest naming one fails validation instead of loading without it. A build
system's hostIntegration is a separate declaration with the same rule; see
Host integrations under Build systems.
A plugin that declares a language owns its process-backed features: the host
stops running its built-in tools for that language, so a missing or disabled
plugin never falls back to them. A feature that none of the plugin's
executionProviders supplies is unavailable for the language.
A language server may declare workspaceConfiguration, an object whose
string leaves may be ${setting:<id>} templates.
Core expands it with the merged settings
(plugins.declarative.workspaceConfiguration, and the launch plan's
workspaceConfiguration), answers the server's workspace/configuration
requests by section path from it, and sends
workspace/didChangeConfiguration after initialized. Hosts send a new
value with lsp.updateWorkspaceConfiguration when an immediate setting
changes; the running server receives it without a restart.
A plugin backend drives its own server only through what the server declares. A server may declare:
| Field | Meaning |
|---|---|
requestMethods |
LSP methods the backend may send with languageServer/request or languageServer/notify; at most 64. |
commands |
The workspace/executeCommand commands the backend may run; at most 128. Declared exactly when requestMethods lists workspace/executeCommand. |
serializedCommands |
A subset of commands that runs one at a time: a request for a command already running joins it and receives the same answer. |
forwardNotifications |
Server notifications the host forwards to the backend; at most 32. |
state |
{scope: "workspace", retention: {maxEntries, maxAgeDays}}. The host prunes the server's per-workspace state directories (see below). maxEntries is 1 to 64, maxAgeDays 1 to 365. |
remote |
supported (default) or unsupported. |
- A name is non-empty text of at most 256 bytes without whitespace or control
characters, unique within its list. Names the host sends itself
(
initialize,initialized,shutdown,exit,textDocument/didOpen,didChange,didClose,didSave,willSave,willSaveWaitUntil,workspace/didChangeWorkspaceFolders, and every$/method) cannot appear inrequestMethodsorforwardNotifications. - A server that lists
requestMethodsorforwardNotificationsrequires thelanguageServer.requestpermission; declaring the permission without such a server isplugin.manifest.invalid. - The launch plan's
sessionPolicyis these fields as sorted lists plusserverID(thelanguageID) andgates. The host sends it unchanged aslsp.startServer.sessionPolicy; Core's LSP engine enforces it, so a host cannot widen it and a backend cannot step outside it. - A server with
remote: "unsupported"is not started for a remote workspace:lsp.startServeranswersnot_supported.
What the engine enforces on lsp.pluginServerRequest and
lsp.pluginServerNotify, the two commands behind the host methods:
- A request must be in
requestMethods, and aworkspace/executeCommandmust carry aparams.commandincommands; anything else isplugin.host.methodNotDeclared. A notification may beworkspace/didChangeConfiguration(the backend always knows the server's settings; the value also becomes the settings the server reads throughworkspace/configuration) or arequestMethodsentry other thanworkspace/executeCommand. The host's own methods stay refused. - The session must belong to the calling plugin
(
plugin.host.foreignContributionotherwise) and be running (plugin.host.languageServerUnavailableotherwise, including every request still pending when the process stops, crashes, restarts, or the plugin is disabled). - A request waits at most
timeoutMilliseconds(default 60000, at most 300000) and ends withplugin.host.timeout; cancelling it sends the server$/cancelRequestand ends it withplugin.host.cancelled. A server's JSON-RPC error isplugin.host.languageServerErrorwithdetails: {code, message}; the server'sdatais not forwarded. A result over the host's result bound isplugin.host.limitExceeded. - A gate is
{id, until: "notification", method, match}, declared by the launch answer. It opens when the server sends a notification with thatmethodwhose params contain every member ofmatch(nested objects match the same way). Until every gate has opened,requestcalls wait in order, at most 128; the 129th isplugin.host.busy. A waiting request still obeys its timeout and cancellation. Notifications are never held. - The first request for a
serializedCommandscommand runs; a later request for the same command, while the first is running or waiting, joins it and is answered with the first one's outcome. Cancelling the first request ends the joined requests with it. forwardNotificationsreach the backend aslithe/host/languageServer/didReceiveNotification.workspace/didChangeConfigurationis the only notification Core consumes itself.
State and retention. The host keeps one state directory per workspace for
each server, under the plugin's storage (never under the package or application
bundle), and gives the server its own as workspaceStateDirectory. The
directories survive disabling and updating the plugin. The plugin chooses the
sub-directory names inside its workspaceStateDirectory; the host chooses the
names of the per-workspace directories themselves and never looks inside them.
The host refreshes a workspace directory's modification time on every start of
the server and sweeps in the background after a start. It asks
plugins.languageServers.stateRetention with {retention, nowUnixSeconds, entries: [{name, lastModifiedUnixSeconds}], activeNames}, where entries are
the per-workspace directories it found and activeNames are those of open
workspaces, and deletes the expiredNames it returns: a directory older than
maxAgeDays and, of the rest, all but the newest maxEntries that open
workspaces leave free. A workspace that is open is never pruned. Names that are
not a single path component are ignored, and the host deletes only names it
listed. A plugin that declares no state.retention keeps everything.
The host can also discard the state of one workspace; the server rebuilds it the next time it starts. That is an engine path only, with no user-facing action.
Preparation status. languageServer/setStatus reports how ready the
server is. It exists because lithe/function/progress belongs to a call that
is running, while preparation (importing a build, indexing) outlives any call.
Example package: fixtures/languages/language-server-backend-launch.plugin.json.
Every plugin-owned process may negotiate capabilities.experimental.litheLogging:
the host offers { "versions": [0] } in initialize, and the SDK answers
{ "version": 0 }. The official SDKs require that offer. The protocol has no
version of its own: its version is pluginAPIVersion, and it is negotiated
independently of the other runtime protocols.
After successful initialization, the backend sends a JSON-RPC notification
lithe/log. Its params contain only these fields; unknown fields are rejected:
| Field | Meaning |
|---|---|
kind |
operationFinished, operationFailed, degraded, or recovered. These select INFO, ERROR, WARN, and WARN catalog events respectively. |
operation |
A registered function ID (up to 64 bytes), function-call for a longer registered ID, a supported lithe/build/* or lithe/vcs/* request method, lithe/keymap/import, or the SDK-owned stages initialize, shutdown, notification, result-encoding. Arbitrary display labels and user input are refused. |
durationMs |
Required for finished/failed, absent for degraded/recovered. Monotonic elapsed integer milliseconds, 0 through 9007199254740991. |
code |
Required except for finished. Stable category, 1–64 ASCII bytes from letters, digits, ._:/-$; never an exception message, command line, query, or response body. |
count |
Optional affected item count, a nonnegative JSON safe integer. |
operationId |
Optional originating wire operation ID, 1–128 ASCII token bytes. The SDK obtains it from the call context. Core resolves a still-active wire request to its host operation; unknown or retired IDs provide no correlation. |
The notification must precede the corresponding RPC response so its wire ID still belongs to the active host operation. Cancellation is an expected result and does not produce an operation failure. The SDK owns automatic function and domain request terminal facts; plugin handlers must not duplicate them. A handler records a warning only when it actually recovers or returns a degraded result. Frequently repeated read/preview/poll functions suppress successful terminal facts; their failures remain observable.
Core supplies the installed plugin ID and routes these facts to
plugins/<id>/host. Plugins cannot provide an event ID, channel, severity,
message, arbitrary fields, error body, or writer path. The fixed catalog events
are plugins.operation.finished, plugins.operation.failed,
plugins.operation.fallback_selected, and plugins.operation.recovery_finished.
Malformed or unnegotiated input produces one category-only
plugins.diagnostics.rejected warning per process session, without retaining
the rejected payload. Ingress admits at most 600 facts per minute per session,
then uses the existing bounded Core handoff to the host's single writer;
overload increments its dropped counter. No polling timer or extra writer is
created. Fixture: fixtures/backend/plugin-logging.json.
Declarative contributions have no plugin process to send lithe/log, so whoever
executes the declared work records the same plugins.operation.finished and
plugins.operation.failed events on the same plugins/<id>/host channel. A
declarative plugin writes no logging code. Each start of a languageServers[] or
debugAdapters[] entry is one languageServer/start or debugAdapter/start
operation, from launch resolution until initialize succeeds. Each run of a
buildSystems[] model or dependencies command source is one lithe/build/model or
lithe/build/dependencies operation, using the SDK request names. A failure
carries one code:
| Code | Meaning |
|---|---|
host.planFailed |
The declared command could not be planned |
host.executableUnavailable |
The declared executable or launch input could not be resolved |
host.launchFailed |
The resolved process could not be started |
host.initializeFailed |
A started server or adapter failed its initialize handshake |
host.timedOut |
The attempt exceeded its deadline |
host.exitedEarly |
A server or adapter exited before initialize succeeded |
host.commandFailed |
A build command exited unsuccessfully |
host.outputInvalid |
A build command's output is not a valid document |
User cancellation and superseded attempts record nothing. Plugin output, paths,
and error text never appear in these facts. The executing owner and the
host-facing command are specified as diagnostics.recordPluginOperation in
rust-core-api.md.
Severity follows the actual result. An operation that failed for the user is
ERROR, a recovery or fallback is WARN, and a useful successful user operation
is INFO. The source stream is not a severity: unclassified stderr is DEBUG
evidence, while standard window/logMessage/window/showMessage types retain
their explicit error/warning/info/detail semantics through separate catalog
events. Raw stdout is reserved for protocol frames. Source code, credentials,
SQL, HTTP headers/bodies, terminal history, protocol payloads, and command
output must never be included in diagnostic fields.
Native upstream files may use ${logDir} only when the host supplies the
current local writable session directory. They remain separate files, not
structured catalog events. Keep metadata and caches in ${stateDir} or
${workspaceStateDir}, and never infer a writable log path from ${pluginDir}.
These runtime files do not modify installed resources or participate in build
resource reuse. Hosts without a local logDirectory reject launches requiring
${logDir}.
A plugin may declare options the user can change, one set per plugin and
shared by every workspace. The host shows them on the plugin's page, stores
the values, and passes the stored values as settings (setting ID to JSON
value) with each launch plan request. A change restarts the plugin's processes
unless the setting's effect says otherwise.
"settings": [
{ "id": "heapMB", "type": "integer", "title": "Maximum memory",
"default": 2048, "minimum": 512, "maximum": 8192, "unit": "MB" },
{ "id": "logLevel", "type": "enum", "title": "Log level", "default": "info",
"options": [{ "value": "info", "title": "Information" }, { "value": "debug", "title": "Debug" }] },
{ "id": "telemetry", "type": "boolean", "title": "Send usage data", "default": false }
]| Field | Meaning |
|---|---|
id |
Starts with a lowercase letter, then letters and digits. Unique within the plugin. |
type |
integer (requires minimum and maximum, optional unit), enum (requires one to sixteen options, each {value, title}; values contain only letters, digits, ., _, -), boolean, multiSelect (one to 32 options; the value is an array of distinct option values), text (requires pattern, matched against the whole value; optional maxLength, default 256 and at most 1024 Unicode scalars; optional placeholder), or file and directory (an absolute path of at most 1024 Unicode scalars, or empty for "not set"; optional placeholder). Text and paths never start with - and contain no control characters. |
title, description |
Text the host shows. Write it in English; the package may translate it for other interface languages. |
default |
Value used until the user changes it. Its JSON type follows type, and it must be accepted by the declaration. |
Because every value is a number, a boolean, or one of the declared option
values, a setting can never inject extra arguments or change which package
file runs. A stored value the declaration no longer accepts (for example after
an update narrowed a range) is replaced by the default; a launch never fails
for it. Values for undeclared IDs are ignored. A template that is exactly one
${setting:<id>} keeps the setting's JSON type, so an initializationOptions
value can be a number or boolean; inside longer text the value is substituted
as text. Declaring settings, or referring to an undeclared one, is refused
with plugin.manifest.invalid or plugin.template.invalid. The settings
declaration is not part of an install review's program list, because no value
can change what runs beyond what the templates already show.
A setting may also say where it is shown and when a change takes effect, so
a plugin can place its options anywhere in the host's settings without the host
knowing the plugin. Hosts keep only generic registration points: a page per
language and per build system, sections, one control per setting type, a
project/global layer switch, and buttons. What appears there comes from the
manifest. Fixture: fixtures/settings/settings.plugin.json.
"settings": [
{ "id": "dialyzer", "type": "boolean", "title": "Dialyzer", "default": true,
"scope": "project", "effect": "immediate", "page": { "languageID": "elixir" } },
{ "id": "mixEnv", "type": "text", "title": "MIX_ENV", "default": "dev", "pattern": "[a-z]+",
"scope": "project", "page": { "languageID": "elixir" }, "section": "Server" }
],
"settingActions": [
{ "id": "rebuildPlt", "title": "Rebuild PLT", "function": "dialyzer.rebuild",
"page": { "languageID": "elixir" }, "confirm": true }
]| Field | Meaning |
|---|---|
scope |
global (default): one value for every project. project: the open project may override it. |
effect |
When a change takes effect. restart (default): the next start of the plugin's processes. immediate: pushed to the running language server through workspaceConfiguration. reload: the next build-model reload; hosts reload the plugin's build models. nextRun: the next run or build command. |
page |
{ "languageID": … } or { "buildSystemID": … }: the settings page that shows the setting besides the plugin's own page. Any plugin may add to a page; the host shows the page while an enabled plugin contributes to it. |
section |
Heading the setting is grouped under on its page. |
settingActions (at most 16, sorted by unique id) are buttons the host
draws on the plugin's page and on their page. Each invokes the plugin
function function of the same plugin with empty params, served by the
plugin's own provider process for the open project (see
plugin functions). confirm: true makes the host ask
first. The host owns the button and knows nothing about what the function
does. Declaring actions counts as using settings.
Placement and use rules, checked by Core at validation:
- An
immediatesetting may appear only in a language server'sworkspaceConfiguration; it never reaches a command line orinitializationOptions, so a change never needs a new process. - A
text,file, ordirectorysetting is a whole template value or the value of onename=argument, never part of a longer argument.
Layers. Hosts store a global value per plugin and setting, and a project
value in the project's .lithe/plugin-settings.json
({pluginID: {settingID: value}}, meant to be shared through version
control). They send both as settings and projectSettings; Core merges
them as project (for scope: "project" only), then global, then default,
skipping a stored value the declaration no longer accepts.
plugins.declarative.resolveSettings returns each effective value with
its source (project, global, or default).
A package may translate its setting text with l10n/<language>.json files
(schema), where <language> is a language tag such
as zh-Hans or ja. plugin.json remains the text for every language without
a translation. Fixture: fixtures/settings/l10n/zh-Hans.json, with golden cases
in fixtures/settings/localization-cases.json.
{
"settings": {
"dialyzer": { "title": "Dialyzer 静态分析" },
"checks": { "title": "检查项", "options": { "format": "格式检查" } }
},
"sections": { "Server": "服务器" },
"settingActions": { "rebuildPlt": { "title": "重建 PLT" } }
}| Member | Translates |
|---|---|
settings.<id> |
The setting's title, description, placeholder, and unit; options.<value> is the title of an enum or multiSelect option |
sections.<heading> |
A section heading, named exactly as section writes it |
settingActions.<id> |
The action's title and description |
Every member is optional, and text a file leaves out stays as plugin.json
writes it. IDs, option values and their order, stored values, and the grouping
and order of sections are the same in every language. For the interface
language zh-Hans-CN the host reads l10n/zh-Hans-CN.json, l10n/zh-Hans.json,
and l10n/zh.json, and shows the first one Core accepts, so one zh-Hans file
serves every region.
Core refuses a whole file, never part of it, with
plugin.settings.invalidLocalization when:
- it is not a JSON object, is larger than 1 MiB, repeats a key, or holds
null; - an entry under
settings,sections, orsettingActionshas a field this format does not define; - it names a setting, option value, heading, or action the manifest does not
declare, gives
optionsto a setting without options, or translates a field the manifest leaves out; - a title, placeholder, unit, or heading is empty or longer than 120 characters, or a description is empty or longer than 480.
The host then tries the next candidate and finally shows the manifest text, so
a broken translation never hides a setting; it logs the refusal for the
plugin's author. Core ignores top-level members it does not know, so a later
host can read translations of more plugin text from the same files.
Translations need no capability or requires entry: a host that does not read
them shows the plugin.json text.
run and test each name an executable: exactly one of a bare command
resolved on the host's tool path, or a host-owned project toolchain ID such
as project-go. The working directory is always the workspace root.
test also declares:
| Field | Meaning |
|---|---|
frameworkID |
Stable framework label such as phpunit or go. |
workspaceLabel |
Label of the "all tests" item and plan. |
projectMarkers |
File names, any one of which makes the workspace a test project. |
testFilePatterns |
Case-insensitive file-name patterns using only *. |
workspaceArguments, fileArguments, testCaseArguments |
Argument templates per scope. |
Argument templates are argv elements, never shell text. Each element is a
string or {"value": …, "when": "fileSelected" | "noFileSelected"};
conditional elements are only allowed in testCaseArguments, where a file is
optional.
| Variable | Available in | Value |
|---|---|---|
${file} |
run, file, test case | Workspace-relative file. A name starting with - becomes ./<name>. |
${fileDirectory} |
run, file, test case | ./<directory> of the file, or . at the workspace root. |
${testName} |
test case | Test identifier with surrounding whitespace removed. |
${testName:regex} |
test case | The identifier with regular-expression metacharacters escaped. |
${userArguments} |
run | The user's program arguments; must be a whole element and may expand to zero elements. |
In testCaseArguments, an element that uses ${file} or ${fileDirectory}
must be guarded with "when": "fileSelected", and some element must use the
test name.
Discovery returns {id: "<language>:workspace", kind: "workspace"} followed by
{id: "<language>:file:<path>", kind: "file", relativeFilePath} for matching
files sorted by path, or an empty list when no project marker is present.
Every version in this contract is 0. A change adds a version to exactly one
place, so a plugin states what it needs and a host states what it runs, each
per capability.
- Capabilities start at
0. A manifest lists the capabilities it uses inrequires, each with version0. A capability version names one manifest section and, when the capability owns one, the runtime protocol that goes with it. - A runtime protocol has no version of its own. Its version is the owning
capability's, and a host offers exactly that version
(
{"versions": [<that version>]}; the process answers{"version": <that version>}). The protocol versions today arelitheBuildfrombuildProviders,litheFunctionsfrombackend,litheVcsfromvcsProviders, the toolchain provider protocol fromtoolchainProviders,litheKeymapImportfromkeymapImportProviders, andlitheLoggingfrompluginAPIVersion. All of them are0. - A host declares a capability version when it can serve every form of it. See Host capabilities.
- A future change gives one capability version
1. The manifest section and the protocol move together, a plugin that uses the change lists"<capability>": 1, and every other plugin keeps0and stays installable wherever it was. An additive optional field in an existing section or message needs no new version.
pluginAPIVersion versions only the base fields: schemaVersion, id,
displayName, version, vendor, permissions, activationEvents,
requires, and requiresTools. It is always 0; any other value is refused
with plugin.api.unsupported and field: "pluginAPIVersion".
The other versioned documents start at 0 too:
| Document | Version field | Value |
|---|---|---|
Plugin manifest (schema); it names the file format, and any other value is plugin.schema.unsupported |
schemaVersion |
0 |
| Native UI document (schema) | schemaVersion |
0 |
| Marketplace index (schema) | schemaVersion |
0 |
Canonical build documents (litheBuildModel, litheDependencies, litheTestResults, litheTestDiscovery, litheBuildConfiguration, litheConfigurationEdit) and the modelInputs snapshot |
version |
0 |
plugins.editorActions.decode request |
version |
0 (required; Core accepts only 0, otherwise Unsupported editor action validation version.) |
| Plugin web bridge | window.lithe.version (bridgeVersion) |
0 |
backend member of a launch plan |
version |
the manifest's requires.backend |
The version of an exported or called plugin function (1 to 1000) is not a
contract version: the plugin that exports the function owns it.
"pluginAPIVersion": 0,
"requires": { "buildProviders": 0, "buildSystems": 0, "languageServers": 0, "languages": 0 }requiresis required and lists at least one capability. IDs are lowerCamelCase identifiers of at most 64 characters; versions are non-negative integers.- Core reads
requiresbefore it decodes any section. A capability this contract does not define, or a version other than0, isplugin.api.unsupportedwithfield: "requires.<capability>", even when the manifest also holds a section Core does not know, so the user learns which capability needs a newer Lithe instead of seeing an unknown field. Process start and Executable resolution belong to the base contract and are not capabilities:requires.serverProcessis refused like any unknown capability. requireslists exactly the capabilities the manifest uses. Leaving out a used one, or listing an unused one, isplugin.manifest.invalid. AnybuildSystems[].providerrequiresbuildProviders, whether it starts its own process or shares a language server. A template-only build plugin must omitbuildProviders. Every VCS provider starts its own process and speakslitheVcs, sovcsProvidersis its capability;backendis the capability of the plugin backend.- The base fields change only with a new
pluginAPIVersion.
Every capability has exactly one manifest section, and each runtime protocol belongs to one capability:
| Capability | Manifest section | Runtime protocol | A host that declares it |
|---|---|---|---|
languages |
languages[], with TextMate grammar and configuration |
— | Reads grammars and editor configuration from the package and applies them in the editor without starting a process. |
packageTrust |
vendor.bundledSignatureRequirement: origin-scoped trust for a host-sealed bundled copy of a publisher-signed plugin |
— | Applies that trust rule to the bundled copy; see Trust. |
languageServers |
languageServers[] apart from their launch fields, including workspaceConfiguration and the session fields |
— | Starts the server as Process start says, delivers its workspace configuration, and serves its plugin-owned sessions. |
executionProviders |
executionProviders[] |
— | Runs and discovers tests through the declared commands. |
settings |
settings[] and settingActions[] with placement, scope, effect and page buttons |
— | Shows the declared settings pages and applies their effects. |
ui |
ui: text, web and native tool windows, commands, and backend-only commands |
— | Renders every kind of window and command, and serves the web bridge. |
buildSystems |
buildSystems[] without provider: detection, options, project model, tasks, run entries, tests, diagnostics, dependencies, sync, and declarative preparation |
— | Detects projects, loads the model, and runs the declared commands. |
buildProviders |
buildSystems[].provider, including own-process and shared language-server providers |
litheBuild |
Starts the provider and speaks litheBuild with workspace-owned sessions. |
buildModelInputs |
buildSystems[].modelSharing and modelInputs: optional read-only model inputs |
— | Supplies the model snapshot between enabled workspace plugins. |
vcsProviders |
vcsProviders[] |
litheVcs |
Starts the provider and speaks litheVcs. |
debugAdapters |
debugAdapters[] |
— | Starts the adapter and applies its declared features and defaults. |
outputLinkMatchers |
outputLinkMatchers[] |
— | Turns Run and Test console text into links through the declared patterns. |
projectGenerators |
projectGenerators[], including tool fields |
— | Lists the entries in the New Project window and generates the project. |
backend |
backend: the plugin's own long-lived process |
litheFunctions |
Starts the backend with or without a page, serves the host services its permissions grant, and speaks litheFunctions. |
toolDefinitions |
toolDefinitions[]: tool identities, layouts, discovery and four-layer candidates |
— | Resolves the definitions through the four layers. |
toolchainProviders |
toolchainProviders[]: provider declarations and permission-bound answer validation |
toolchain provider protocol (litheToolchains) |
Starts the provider and validates its answers. |
pluginFunctions |
pluginFunctions: exported functions and declared calls between plugins |
— (the relay methods are part of litheFunctions) |
Relays calls between plugins. |
keymapImportProviders |
keymapImportProviders[]: independent shortcut import providers |
litheKeymapImport |
Starts the provider and imports the shortcuts it reads. |
Validating a capability is not running it. Core validates every capability it
defines on every platform, but each host implements only some of them, so a
host states what it runs as hostCapabilities: { "<capability>": [versions] }
in plugins.declarative.validate, plugins.marketplace.validateIndex, and
plugins.marketplace.installReview:
- A host declares a capability version when it accepts every manifest form of
that version and speaks its runtime protocol. Features a provider offers on
top of that (tests, run preparation, commands) may go unpresented, and a
plugin must not depend on them being called. Host services a plugin needs
are not declared here: it discovers them at runtime with
getCapabilitiesand its permissions. validateandinstallReviewrefuse a manifest whoserequiresnames a version missing fromhostCapabilitieswithplugin.api.unsupportedandfield: "requires.<capability>". WithouthostCapabilitiesthey check only what Core supports.validateIndexoffers a version only when the host and this Core build both support every entry of itsrequires; withouthostCapabilitiesit offers none. A plugin that uses no build systems is therefore offered on a host that cannot run them.
Core validates the versions it supports without a host table, but admission must use the actual host table; Core's protocol support alone does not enable a capability on a host.
- A released capability version never changes meaning, and hosts keep supporting it.
- A plugin declares exactly the capabilities it uses, each at the lowest version that covers what it uses. Raising one without using what it adds only cuts off older hosts.
- A new capability version is added only for a change a host must not ignore. Plugins that do not use the change keep their version and stay installable wherever they were.
- Moving a plugin to a new capability version requires a new plugin version, and its index entry keeps the last version with the previous requirements so older hosts still have a version to install. Published packages are signed and are never rewritten.
- A host release that supports a capability version ships before any plugin version that declares it.
- The manifest holds only what a host must know before it starts a process: what to start, with which permissions, for which files and projects. What a running process can answer is negotiated over its own protocol (see Build providers), never declared in the manifest.
The host starts every process a plugin declares: a language server, a build
provider with its own process, a VCS provider, a debug adapter and the plugin
backend. They share one set of launch fields, one launch-template language and
one launch plan. All of it belongs to the base contract (pluginAPIVersion 0):
no capability is declared to use it.
launch.kind |
Starts |
|---|---|
packaged |
An executable inside the package: {path}. |
systemCommand |
A bare command name the host resolves with its tool discovery: {commands}. |
runtime |
A package file run by a tool-definition's executable, shipped or discovered; see Runtime launch. |
backend |
A language server whose command line the plugin backend answers at each start; see Backend-resolved launch. |
arguments, validationArguments, runtimeDependencies, and
initializationOptions mean what the language server fields
say. A tool is always named by tool-definition ID: the host
has no list of runtimes it knows by name.
{"kind": "runtime", "tool", "packagedRuntime"?, "minimumVersion"?, "runtimeArguments"?, "entry"} starts a file inside the package with a
runtime, so the package needs no launcher script. The runtime is either
shipped by the plugin (packagedRuntime) or discovered by the host.
| Field | Meaning |
|---|---|
tool |
A tool-definition ID that requiresTools lists; the definition decides how the entry is passed (see Tool references). |
packagedRuntime |
Package-relative path of the runtime executable the plugin ships, for example runtime/${arch}/bin/node; ${arch} is the only variable it may use. The host starts exactly this file: it never looks the tool up on the machine, and no user override or other plugin can replace it. A plugin that needs a specific runtime owns it this way, so the host carries no knowledge of where that runtime lives. It excludes minimumVersion. |
minimumVersion |
Lowest acceptable version of a runtime the host discovers: one to three dot-separated numbers such as 21 or 18.17. Only the components it names are compared, so 21 accepts every 21.x. A runtime whose version cannot be read does not qualify. |
runtimeArguments |
Launch templates placed before the entry, such as runtime options. |
entry |
Package-relative entry file. |
The launch tool is already a requirement and must not be repeated in
runtimeDependencies. A runtime launch declares no validationArguments: the
tool definition checks the version of the executable it resolves, and a
candidate that does not satisfy minimumVersion is not used, the same as a
candidate that fails a validation probe.
A launch names every runtime by tool-definition ID, so a
host never learns a tool from a name. The manifest lists every tool it starts a
process with in the top-level requiresTools, and the definitions come from
toolDefinitions of the plugin itself or of another enabled plugin.
| Field | Meaning |
|---|---|
requiresTools |
At most 32 tool-definition IDs, each short (interpreter) or plugin-qualified (org.example.tools:interpreter), without duplicates; stored sorted. |
launch.tool of a runtime launch, runtimeDependencies[].tool, runSelectedRuntime.tool of a debug adapter |
A tool-definition ID that requiresTools lists. An ID it does not list is plugin.runtime.undeclared with the field that names it. |
process.executable.tool of a lithe/host/run/start request |
A tool-definition ID that requiresTools lists. An ID it does not list is refused with plugin.run.invalidStart and field: "process.executable.tool". |
The definition, not the host, decides how its executable starts an entry: its
launch template says where runtimeArguments, the entry and arguments go
(["${executable}", "${runtimeArguments}", "${entry}", "${arguments}"] runs
<tool> <runtimeArguments> <entry> <arguments>, a template without ${entry}
runs no entry). The launch plan of a process therefore returns the parts, not a
command line:
| Field | Meaning |
|---|---|
launch |
{kind: "tool", tool, entry, runtimeArguments, minimumVersion?, packagedRuntime?}. entry is the absolute path of the package file, runtimeArguments are expanded, and packagedRuntime is the expanded path of a runtime the plugin ships (the host then uses that file as the tool's executable and never looks the tool up). |
arguments |
Only the plugin's own arguments; the host places them where the definition's ${arguments} is. |
runtimeRequirements |
{kind: "tool", tool, minimumVersion?} for every tool the host must resolve. |
The host resolves each tool through the four layers of
toolchains/README.md, checks minimumVersion with
toolchains.checkVersion, which checks the version of the executable the
definition resolves, and expands the start with toolchains.instantiateLaunch.
A tool that resolves to no executable is plugin.host.toolUnresolved
(details.reason says why). See fixtures/run/tool-reference-launch.plugin.json.
arguments, validationArguments, runtimeArguments, and the string values
of initializationOptions may use these variables. A package therefore never
contains a machine path, and the host decides where state lives.
| Variable | Value |
|---|---|
${pluginDir} |
Root of the installed, read-only package. It must be used at most once per value and be followed by a /-rooted path inside the package; that path may vary only by ${arch}. |
${stateDir} |
Writable directory the host keeps for this plugin's server and removes when the plugin is uninstalled. Not available in validationArguments. |
${workspaceStateDir} |
Writable directory for the server's state of the current workspace. Not available in validationArguments, nor in a plugin backend, which belongs to no workspace. |
${logDir} |
Absolute current-session plugins/<id>/files/ directory, explicitly supplied by the host as logDirectory. Available for language servers and plugin backends, never in validationArguments. The host creates it in writable log storage, outside the installed package; Core does not derive it from package resources. |
${arch} |
Host CPU architecture: arm64 or x86_64. |
${setting:<id>} |
Current value of a declared plugin setting. Not available in validationArguments, and never inside a package path. |
plugins.declarative.languageServerLaunchPlan expands a server's launch. The
host selects the process with exactly one of languageID (a language server),
buildSystemID (a build provider), vcsProviderID (a VCS provider), and
backend: true (the plugin backend), passes {pluginDirectory, stateDirectory, workspaceStateDirectory?, logDirectory?, temporaryDirectory?, architecture, settings?}, and receives. workspaceStateDirectory may be omitted only when the selected
process does not use ${workspaceStateDir}; otherwise the request is
plugin.launch.invalidRequest. The same error applies when a template uses ${logDir} but logDirectory is absent, empty, or not absolute.
| Field | Meaning |
|---|---|
owner |
{pluginID, version} of the installed package Core validated for this plan. The host starts the server for that exact installation and uses this identity, not one it read elsewhere, when it later relays requests or reports the session. |
launch |
{kind: "packaged", path}, {kind: "systemCommand", commands}, or {kind: "tool", tool, entry, runtimeArguments, packagedRuntime?, minimumVersion?}: what the host resolves to an executable. A runtime launch returns the tool shape of Tool references. A backend-resolved launch returns {kind: "backend", function, timeoutMilliseconds} here, and {kind: "tool", tool} appears in the plan plugins.languageServers.decodeLaunch returns. |
arguments |
Argument list after the executable. A tool-shaped launch returns only the plugin's own arguments. |
validationArguments |
Expanded probe arguments. |
runtimeRequirements |
{kind: "tool", tool, minimumVersion?} for every tool the host must resolve through toolchains, a launch tool included, sorted by tool. A packaged runtime is not listed. Only the launch tool can carry a minimum version. |
initializationOptions |
Expanded options; absent when the server declares none. |
requiredDirectories |
state, workspaceState, log, and/or temporary: the directories the host must create before starting the server. |
requiredPackagePaths |
Sorted package-relative paths the launch points at. The host verifies each is inside the package and not a symbolic link before starting the server. |
settings |
Effective value of every declared setting that produced this plan; absent when the plugin declares none. |
resolveParams |
Present only for a backend-resolved launch: the params of the launch function, without the context and the optional tools the host adds. |
environment, writablePaths |
Present only in the plan decodeLaunch returns: the variables of the process, and the paths it writes. |
sessionPolicy |
Present for a language server that declares session fields: the session policy the host passes to lsp.startServer unchanged. |
buildProvider |
Present only for a buildSystemID selection: {buildSystemID, executesProjectCode, mayUseNetwork, languageServer?}. languageServer names the language whose server is the provider; the rest of the plan is then that server's plan, and the host reuses its running session instead of starting a second process. |
vcsProvider |
Present only for a vcsProviderID selection: the provider descriptor the host keeps for the session. |
backend |
Present only for a backend selection: {hostServices, version}, the sorted host services the plugin's permissions grant, plus version, the manifest's requires.backend (0). The host passes hostServices to lsp.startServer.hostServices unchanged. |
A language without a server, an undeclared build system, a build system
without a provider, an undeclared VCS provider, and a plugin without a
backend are plugin.launch.unavailable. Core never creates or
reads any of these paths.
A server whose command line depends on what the plugin finds when it starts (a version it resolves, a tool it bootstraps, options it derives from the project) lets the plugin backend answer the command line each time instead of declaring one:
"launch": {"kind": "backend", "function": "launch.resolve", "timeoutMilliseconds": 30000}- Only a language server can use it. The plugin declares a
backend.functionis a plugin function ID.timeoutMillisecondsis 1000 to 120000 and defaults to 30000. - The server declares none of
arguments,validationArguments,runtimeDependencies,initializationOptions, orhostIntegration: the answer carries the first four, and a host integration is host code, which this launch exists to avoid.workspaceConfigurationstays declarative unless the answer carries its own. plugins.declarative.languageServerLaunchPlanfor such a server returns the first step, not a command line:launch: {kind: "backend", function, timeoutMilliseconds},resolveParams,requiredDirectories(alwaysstate,workspaceState,log, andtemporary),settings, andsessionPolicy. Its request addstemporaryDirectory, an absolute scratch directory of this start; the launch needs all four directories, so a request without one isplugin.launch.invalidRequest.- The host creates the directories, starts the plugin backend if it is not
running, and calls
functionthroughlithe/function/invokewithresolveParamsand the workspacecontext. A host may also sendtools,{<toolID>: {executable, home, version}}, the resolution of each tool the manifest lists inrequiresToolsthat it could find; Core does not validate it. The host resolves each required tool as an installation with the user's choice and discovery, and leaves out a tool that is missing, unusable, or inside the project;homeandversionare present when the installation has them. A function must not requiretoolsor any entry in it: it answers{kind: "tool"}and lets the host resolve the tool, and treats a missing entry as "configure nothing". A backend usestoolsto point its server at the same installation the host runs, for example as the default runtime of a language server whose Run goes through the tool. A failed or timed-out call, or an answer for a context whose generation has changed, ends this start without a process. - The host passes
{manifest, languageID, request, result}toplugins.languageServers.decodeLaunch, which returns{status: "ok", plan}or a rejection. The plan is an ordinary launch plan (launchispackagedortool) whosesessionPolicyalso carries the answer'sgates; the host starts it like any other. A server that is not launched by the backend isplugin.launch.notBackend, so a host never runs an answer it did not ask for.
resolveParams:
| Field | Meaning |
|---|---|
serverID |
The server's languageID; also its ID in the languageServer host methods. |
platform |
{os, arch}: the host operating system and CPU architecture (arm64 or x86_64). |
stateDirectory, workspaceStateDirectory, logDirectory, temporaryDirectory |
The writable directories of this start, absolute and outside the package and the application bundle. |
settings |
The effective value of every declared plugin setting. |
The answer is an object with these fields; an unknown field is refused.
| Field | Meaning |
|---|---|
executable |
{kind: "packaged", path} is a file inside the plugin package, read-only, listed in the plan's requiredPackagePaths. {kind: "tool", tool} names a tool definition of the plugin; the host resolves it like any tool launch. |
arguments |
At most 1024 strings of at most 32 KiB, without NUL. |
environment |
At most 256 variables; a name is non-empty without =, a value is at most 32 KiB. |
initializationOptions, workspaceConfiguration |
JSON objects of at most 1 MiB each. |
gates |
At most 8; see Plugin-owned sessions. |
writablePaths |
At most 32 paths the server writes. Each lies inside one of the four directories; . and .. components are refused. The plan carries them sorted, and the host creates their parent directories before the start. |
Rejections: plugin.launch.invalidResult for a malformed or oversized answer
or gate, plugin.launch.outsidePackage for a packaged path that leaves the
package, plugin.launch.undeclaredTool for a tool the plugin does not
define, plugin.launch.unwritablePath for a writable path outside the host
directories, plugin.launch.invalidRequest for a request without a usable
directory, plugin.launch.unavailable for a language without a server, and
plugin.launch.notBackend. See fixtures/languages/language-server-backend-launch.plugin.json
and fixtures/languages/language-server-launch-resolution.json.
Limits the Lithe desktop host applies today: a {kind: "tool"} answer is resolved
with the user's tool choice and discovery, and a program that lies inside the
project is refused, because opening a project must not run it; a server whose
launch the backend resolves does not start in a remote workspace; and the
scratch temporaryDirectory is removed when the server process ends.
Declare executable sources in invocation.executables, in preference order.
Array order is the recommended form; no custom path setting is required for a
plugin that only needs automatic discovery. The source kinds are
projectFile, environment, command, runtime, setting and toolchain.
Executable resolution belongs to the base contract: no capability is declared
to use it. setting and the ordering metadata are described below:
"executables": [
{ "id": "selected", "kind": "setting", "settingID": "toolPath" },
{ "id": "wrapper", "kind": "projectFile", "path": "mvnw" },
{ "id": "system", "kind": "command", "command": "mvn" }
]A setting references a declared global file setting with effect: "reload"
or "nextRun". Core receives raw request.settings. An absent
value uses the declared default; an explicit empty string emits a setting candidate
with path: null, which skips. A nonempty value must satisfy the setting rules,
be absolute, and have no surrounding whitespace or controls. Invalid stored
values reject the invocation instead of invoking the normal settings display
resolver's fallback-to-default behavior. Project overrides never select tools.
The host checks file permissions and versions: a configured failure is terminal,
including a failure to probe, and must not fall back to subsequent candidates or
a Provider's own lookup. Paths with spaces remain single paths. The selected
setting's directory precedes PATH. Runtime candidates construct their environment
before version probing and reuse it for execution; a rejected candidate's
environment does not leak into the next choice. Provider hints and task/test
execution use this same plan/selection mechanism. If resolution fails before a
configured candidate is reached (for example, an earlier Runtime is missing),
any nonempty setting in the tool or Runtime candidate sets still forbids the
Provider's independent lookup. All-empty settings retain automatic-discovery
fallback. This check applies after failure, so an earlier successful candidate
is unaffected by a later unused setting.
Optional metadata applies within one candidate set owned by one plugin:
idis a unique stable identifier; anonymous candidates cannot be anchors.priorityis a signed 32-bit integer; higher wins, absent means 0. Ties retain original array order.insertBeforeorinsertAfternames an ID in this set. One candidate cannot combine either withpriority, or declare both insertion fields. Constraints mean relative order, not immediate adjacency.- Normalize the complete set into a directed graph before platform filtering. Repeatedly select a zero-indegree node by descending priority, then original index; emit it and remove its edges. Explicit edges take precedence over priority. Duplicate/invalid IDs, unknown anchors, cycles/self-edges and conflicting metadata are manifest errors. Filter platforms only after sorting, so anchors remain valid even if they do not apply to the current platform.
lithe_plugin_model::executables owns the reusable source types and deterministic order;
ExecutableCandidateResolver receives file-system, Setting (already validated by
Core), Environment, Runtime and command-discovery capabilities from the host.
Results distinguish skip, resolved and terminalFailure; plugins declare data
and cannot inject host native code. Each operation uses a value snapshot for its
settings/environment and ordered plan. Passive import does not resolve/probe
runtimes or execute project wrappers. A project candidate remains project code
and is only executed through the consumer's existing trust/user-action workflow.
Executable candidates are used by build systems and their custom runtime
dependencies. Own build providers adapt command/runtime launches through
BuildToolResolver and receive the selected build tool as tool.executable;
their process protocol is unchanged. Language servers, execution providers,
and other launch forms name a tool definition instead of
declaring a candidate list. Any launch form adapted to this resolver must
preserve the package-realpath/executable checks, per-operation consent (there
is no workspace trust state yet), auto-start restrictions, runtime ownership
and cancellation.
See build-tool-path.plugin.json.
A candidate may use
{ "kind": "toolchain", "toolchainID": "org.example.compiler:compiler" }.
Core accepts the same short/qualified ID grammar as tool definitions and adds the
normalized instance root. The host adapter resolves installed definitions
against fresh native evidence and directory pins. Unavailable selections are terminal,
including when a prerequisite runtime fails first; they cannot silently fall
back to Provider discovery.
A host-managed node or go dependency may select one such candidate,
while retaining Core's version probe. Any other dependency that declares no
executables is already this candidate for the definition with its own ID, and
that definition checks minimumVersion. A tool that needs another tool as a
prerequisite names that tool's definition; Core adds no registry entry for any
particular tool.
Passive import performs no version or runtime probes; a definition that requires
verification is not reported as an already verified import executable.
A toolchain that resolves to a project file remains project code,
including through a symlink, and native configuration requires explicit Read
before running it. See build-toolchains.plugin.json.
| Permission | Required when |
|---|---|
languageServer.launch |
languageServers or vcsProviders is not empty, or a build provider starts its own process |
process.run |
any provider declares run |
process.test |
any provider declares test |
process.build |
buildSystems is not empty |
process.vcs |
vcsProviders is not empty |
debug.launch |
debugAdapters is not empty |
debug.attach |
an adapter lists attach in requests (and only then); it also covers attaching to a remote host and port |
project.generate |
projectGenerators is not empty |
ui.contribute |
ui is declared |
ui.web |
a tool window shows web content |
backend.launch |
backend is declared |
keymaps.provide |
keymapImportProviders is nonempty; permits offering mappings, not reading files by itself |
keymaps.readSelectedSources |
keymapImportProviders is nonempty; permits reading only declared files in an explicitly selected import source |
credentials.store |
the backend uses the credentials host service; refused without a backend |
workspace.metadata |
the backend uses the workspace host service; refused without backend |
editor.actions |
the backend registers independent editor actions; requires backend and documents.read, grants only editor/registerActions, not document edits or domain execution |
editor.providers |
the backend supplies editor language features for matching files; requires backend and documents.read, grants only editor/registerProviders, not document edits |
terminal.create |
the backend creates and manages its own native terminals, including bounded display history for reopen; requires backend |
run.start |
the backend starts and stops host-owned Run sessions; grants lithe/host/run/start, lithe/host/run/stop and the lithe/host/run/didExit notification; requires backend |
debug.start |
the backend starts a debug session on one of its own adapters; grants only lithe/host/debug/start; requires backend and a declared debugAdapters entry |
languageServer.request |
a language server lists requestMethods or forwardNotifications, and only then; requires backend. Grants the languageServer host methods for the plugin's own servers and their two notifications, nothing else |
documents.contentProvider |
the backend registers a content provider; requires backend. Grants documents/registerContentProvider only, not reading or editing documents |
| Activation event | Meaning |
|---|---|
onLanguage:<id> |
Start the language server when a document of the language opens in a project. |
onRun:<id> |
Activate the run provider when the user runs a file of the language. |
onTest:<id> |
Activate the test provider when the user lists or runs tests. |
onBuildSystem:<id> |
Activate the build system before its first plan, model, or decode. |
onVcsRoot:<id> |
Start the VCS provider once a root of its VCS is detected in the workspace. |
onDebug:<id> |
Start the debug adapter when a debug session needs it. |
onWorkspaceOpen |
Start the backend process once a workspace is open. Optional, names no ID, refused without backend. |
Each provider needs its event and each event needs a provider; the only
optional event is onWorkspaceOpen. There is no event that starts a plugin
on every launch (onStartupFinished, *): it is
plugin.activation.invalid. Enabling a
plugin only permits activation; nothing starts until its event occurs.
publisherPackage: the package must carry a valid publisher signature (lithe-plugin-signature.json, Ed25519 over every file) from a key the host trusts. Development builds may install an unsigned package from disk as a local development package; release builds refuse it.sameTeamAsHost: only honored for a package shipped inside the host's own signed installation, whose code signature already seals it.none: a community package without a required Lithe signature. A host may install it from a third-party marketplace source after matching the archive against the index checksum, or from a user-selected local ZIP or unpacked directory after taking a private snapshot. Both paths require the user to confirm the install review. IDs in the official namespace (dev.lithe,dev.lithe.*) may not use it (plugin.manifest.invalid). A signature document, when present, must still be verified; community trust cannot silently ignore an invalid signature.
A package can declare requires.packageTrust: 0 and
vendor.bundledSignatureRequirement: "sameTeamAsHost", together with
vendor.signatureRequirement: "publisherPackage". Only official-namespace IDs
may use this declaration. Both the primary and bundled policies must match the
host's pinned official catalog. Only a package whose actual resolved path is
inside the host's sealed installation and whose origin is bundled may omit
the publisher signature. Every independently installed, imported or rolled-back
copy still needs the publisher signature. A present signature is always checked,
even inside the host seal. The optional declaration alone grants no trust.
A host that does not declare packageTrust: 0 in hostCapabilities filters this
version before download and rejects its manifest. A manifest without the
declaration gets no bundled exemption. The valid example is
fixtures/package-trust/publisher-bundled.plugin.json.
A host pins the signature requirement of its official plugin IDs so a package
cannot downgrade itself. Selecting a file from disk never overrides that pin,
namespace restrictions, publisher-key checks, or the installation seal required
by sameTeamAsHost. A development build may permit an unsigned publisher package
as localDevelopment; release builds refuse that origin. Community none
packages use their own approval path in both build configurations and must not
be reclassified as development packages.
A marketplace source is a JSON index (plugin-index.schema.json) that lists
plugins and their release archives. Hosts fetch, cache, and download; Rust
Core decides validity and availability through plugins.marketplace.* so
every product shows the same catalog and the same install review.
- The host ships one official source: the index at
Plugins/marketplace/index.jsonin the Lithe repository. A copy is bundled read-only with the application so the catalog works offline; refreshing fetches the same file fromhttps://raw.githubusercontent.com/Lithe-IDEA/Lithe-IDEA/HEAD/Plugins/marketplace/index.json. - Hosts also preset one community source, Lithe Store
(
https://github.com/Lithe-IDEA/Lithe-store, indexhttps://raw.githubusercontent.com/Lithe-IDEA/Lithe-store/HEAD/lithe-plugin-index.json). It is a third-party source in every respect (community trust,dev.litheIDs refused), enabled by default, and removable; a host remembers the removal instead of adding it back. - Users may add third-party sources.
plugins.marketplace.resolveSourceturns what the user pasted into the index URL to fetch: an https index URL is used as is (fragment removed);https://github.com/<owner>/<repo>resolves tolithe-plugin-index.jsonat the repository root on its default branch (HEAD); a…/blob/<ref>/<path>page resolves to its raw file. Plain http, embedded credentials, and other GitHub pages are refused withplugin.source.invalid. - Hosts cache fetched indexes in platform cache or application-support storage, never inside the installation.
| Field | Meaning |
|---|---|
id |
Plugin ID; must equal the package's plugin.json id. |
name, author, description, license |
Display metadata; must not be empty. |
localizedDescriptions |
Optional translations of description, keyed by language tag such as zh-Hans. Hosts show the entry matching the interface language and fall back to description. |
repository |
https://github.com/<owner>/<repo> that owns the plugin's releases and issue tracker. |
homepage |
Optional https URL. |
tags |
Optional single words, lowercased and sorted by Core. |
icon |
Optional raw.githubusercontent.com file of the same repository. |
versions[] |
version, required pluginAPIVersion, required requires (the package manifest's requires; a capability or capability version this contract does not define is allowed and makes the version unavailable), releaseDate (YYYY-MM-DD), and downloads[]. |
downloads[] |
platforms (for example darwin-arm64, darwin-x64, windows-x64, windows-arm64), downloadURL, and the archive's lowercase hex sha256. A platform may appear in at most one download of a version. |
A downloadURL must be a .zip GitHub Release asset of the entry's own
repository (https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>.zip,
owner and repository compared case-insensitively), so an index cannot point a
plugin at another project's release.
plugins.marketplace.validateIndex accepts
{ "index": object, "sourceKind": "official" | "thirdParty", "platform": string, "hostCapabilities"?: object }.
A malformed index root or a schemaVersion other than 0 rejects the whole
index. Otherwise it returns { "status": "ok", "index": { "plugins", "rejected" } }:
rejectedlists refused entries in index order with theirerror. A third-party source listing an official-namespace ID is refused withplugin.index.officialNamespace; a repeated ID keeps the first listing and refuses later ones withplugin.index.duplicate.pluginsis sorted by ID. Each accepted entry is normalized, listsversionsnewest first (each with itsrequires), reportsplatforms,latestVersion, andlatestReleaseDateof the newest version, and anavailability:{status: "available", version, pluginAPIVersion, releaseDate, downloadURL, sha256}for the newest version with this platform's archive that the host can run:pluginAPIVersion0and every capability of itsrequireshosted at the version it names (see Versioning);{status: "unsupported", code: "plugin.index.apiUnsupported", platform, message}when the platform is listed only by versions that need a capability, a capability version or apluginAPIVersionthe host does not run;{status: "unsupported", code: "plugin.index.platformUnsupported", platform, message}when no version lists the platform. Hosts show "not supported on " instead of an install action.
- Official: an entry is official only when it comes from the official source and its package passes the Lithe publisher signature.
- Community marketplace: entries from third-party sources. Hosts install
in this order: verify the archive SHA-256 against the index
(
plugin.archive.checksumMismatch), validate the declaration and package, runplugins.marketplace.installReview, show the review, and install only after the user confirms. The index checksum proves a match to that index's bytes, not an authenticated publisher identity. - Community local disk: a user-selected ZIP or unpacked directory has no marketplace index or authenticated repository. The host first copies a private snapshot, then validates and reviews that snapshot with the same Core command. A local checksum must never be shown as a repository match.
plugins.marketplace.installReview accepts
{ "manifest": object, "expectedID"?, "expectedVersion"?, "approvedPrograms"?: [object], "hostCapabilities"?: object }
and returns { "status": "ok", "review": { id, version, permissions, programs, newPrograms } }.
A package whose ID or version differs from the index entry is refused with
plugin.package.mismatch, and a package that requires a capability version
missing from hostCapabilities with plugin.api.unsupported. programs lists everything the host may start for
the plugin, sorted by kind, language, and tool:
kind |
Fields | Meaning |
|---|---|---|
packagedLanguageServer |
languageID, path, arguments, validationArguments |
Executable inside the package, with its launch arguments and validation probe |
systemLanguageServer |
languageID, commands, arguments, validationArguments |
Command resolved by host tool discovery |
runtimeLanguageServer |
languageID, tool, packagedRuntime, entry, runtimeArguments, arguments, validationArguments |
Package entry started by a runtime; packagedRuntime is null for a discovered runtime |
buildSystem |
buildSystemID, invocation, options, tasks, run, test, model, dependencies, provider? |
Every command line the build system can plan; provider is the shared form, which sends build requests to a language server |
packagedBuildProvider, systemBuildProvider, runtimeBuildProvider |
the matching language-server fields with buildSystemID, executesProjectCode, mayUseNetwork instead of languageID |
A build provider with a process of its own |
packagedVcsProvider, systemVcsProvider, runtimeVcsProvider |
the matching language-server fields with vcs, vcsTool, mayUseNetwork instead of languageID |
A VCS provider process; vcsTool is the declared VCS client or null |
runtime |
tool, minimumVersion? |
Runtime the host discovers and passes to a language server, build provider, or VCS provider |
run |
languageID, executable, arguments |
Run command and its argument template |
test |
languageID, executable, workspaceArguments, fileArguments, testCaseArguments |
Test command and its argument templates |
projectGenerator |
generatorID, command, arguments, environment, collect, mayUseNetwork, timeoutSeconds, when |
A command step of a project generator; files and archive steps start no program |
Each entry carries everything that decides what actually runs, so an update
that keeps the same executable but changes its arguments, argument templates,
or validation probe is a new program; language-server and build-provider
entries also carry initializationOptions when declared. Hosts show all
permissions and the complete programs, and persist each approved program
as canonical JSON without dropping fields. A display may use readable labels
but must retain all canonical content, including a full JSON fallback for
program kinds it cannot describe completely. Recognized kinds may keep
their readable summary and offer a “Full Program Declaration” disclosure
with the complete canonical JSON; unknown kinds already render that JSON and
do not need a duplicate disclosure.
Local disk community installation requires explicit confirmation on every first
install, update, reinstall, and same-version replacement, including unchanged
programs and permissions. An empty newPrograms never waives disk confirmation.
Marketplace installation retains its existing policy: first install, missing
previous approval, or nonempty newPrograms requires confirmation; a marketplace
update with no new programs may reuse its recorded approval. A host may pass
previous approvedPrograms to highlight differences, but a confirmation always
shows the complete current review. Core's review remains a pure description and
comparison, not an approval grant.
When several marketplace sources list the same ID, the user chooses the source
at install time and later updates come only from that source. Local disk
provenance is never a marketplace source or an automatic update subscription.
A host's disk entry accepts a ZIP or an unpacked package directory. It does
not run code from the selection. The host's importer copies into a
fresh private directory (mode 0700) before declaration validation, trust
checks, review, or installation. It refuses aliases, symbolic links, special
files, ambiguous manifests, and detected source changes during copying, and
applies archive/extracted-size and entry-count limits to the snapshot. Directory
copy depth is limited to 64, and its temporary root must stay outside the
selected source. Before extraction, ZIP preflight checks central and local
header paths/types and rejects duplicate or colliding paths.
The host injects fileStorage.temporaryDirectory(). Within that
platform-owned writable root the importer creates
lithe-plugin-disk-<UUID>/archive.zip for a ZIP and package/ for copied or
extracted files. archiveSHA256 is computed from the copied ZIP, not by reading
the original after review; an unpacked directory has no archive and this field
is absent/nil. The content digest covers the package files. Neither digest
authenticates a publisher or remote repository.
Disk digesting, Rust review, and candidate validation run on a native worker, with a worker-owned Store decoder and immutable validation policy. The UI actor remains available to close the page or cancel. Cancellation is checked between review stages and before publishing the prepared candidate; snapshot cleanup waits for the worker to finish reading it.
PluginDiskProvenance is host-owned local installation data, separate from
PluginMarketplaceProvenance; it is not a new field in plugin.json or the
marketplace index schema. A disk record contains the selected fileName,
optional archiveSHA256, contentDigest, and complete canonical
approvedPrograms. Marketplace provenance instead retains its source ID/name,
repository, index archive checksum, content digest, and approved programs.
Each community version must have exactly one of these provenance records;
missing or conflicting provenance is rejected.
The store rechecks the content digest of the installation staging copy before
publishing it, on every installed-package scan, and before rollback. Changed,
added, or removed package files reject that candidate or version. Active and
previous versions retain their own origin and provenance (disk /
previousDisk, marketplace / previousMarketplace); rollback verifies the
restored version against its own approval. Replacing the active version with
the same version retains the existing previous-version history; it does not
create a second rollback slot for the overwritten same-version bytes. A
marketplace entry with the same ID must not automatically offer updates or
reinstall a disk-managed installation.
The importer owns partial-copy cleanup on failure or cancellation. After
preparation the service owns the snapshot until review cancellation or install
success/failure and discards it in all these cases. Closing the plugin page
cancels its disk import and ends the installation session: pending snapshots
are discarded immediately and late prepared results are discarded rather than
installed. The package store owns its separate Application Support staging,
version directories, and installation records. None of these snapshots, staging files, installed versions, or approval
records are reusable worktree build resources: a content hash does not transfer
user consent or provide a version/platform/architecture/toolchain identity stamp.
They are excluded as plugin-disk-install-state. No runtime write targets the
signed app bundle or Windows installation resources, so these paths do not
change code signatures or Sparkle differential-update baselines.
Static checks alone do not verify the UI confirmation, complete program rendering, cleanup, or bundle hashes in a real workflow.
buildSystems describes a project-level build tool (Cargo, Go, Gradle,
Maven, …) as data. Every build tool gets the same model and the same
commands: Core validates the declaration, detects instances, decodes the
model, and expands every command line; the host only runs the processes Core
planned and passes their output back. A plugin therefore needs no
host change to add a build tool, and no host code branches on a build-system
ID. One complete fixture covers each plugin-described model source:
build-static-file reads a project file, build-command-stdout decodes a
command's standard output, and build-command-output-file decodes a file the
command writes. They use Cargo, Go, and Gradle as realistic inputs;
they are not those plugins. build-provider and build-provider-language-server show the two
forms of a build provider, and fixtures/build-systems/build-plans.json is the golden list
of plans and decoded outputs every host gets from them.
A build system needs buildSystems: 0 in requires,
the process.build permission,
and the activation event onBuildSystem:<id>. IDs of build systems, tasks,
and run entries start with a lowercase letter and use lowercase letters,
digits, and hyphens. Option IDs are camelCase like setting IDs. Paths in a
declaration are relative, use /, and stay inside their base directory
(plugin.path.invalid otherwise).
| Field | Meaning |
|---|---|
detection |
markers are file names; a directory holding one is a candidate root. Core searches at most 6 levels below the workspace root and skips known caches and build outputs, plus directories Git ignores: the rules come from the .gitignore files inside the workspace and the info/exclude of each repository whose root lies in it, not from directories above the workspace root, core.excludesFile or the index, so a tracked manifest below an ignored directory is not a candidate. Detection also skips the workspace's ignored paths, a Build option the user edits (.gitignore patterns stored in the team or machine-local Build options): by default every directory whose name starts with a dot, such as .claude/worktrees, and the user can list more or bring one back with !. nesting: "outermost" folds deeper candidates into the outer instance (Gradle builds, Maven reactors); "separate" (default) keeps each (Cargo packages). priority (0–100, default 50) decides between plugins declaring the same ID; ties go to the smaller plugin ID. |
invocation |
executables in preference order: projectFile (a wrapper inside the root, with requires files; always runs project code), environment (a path below a variable listed in environmentInputs), command (a bare name found on PATH), runtime (a declared runtime dependency itself). platforms limits a candidate. globalArguments precede every command. version probes the executable with literal arguments and a pattern whose first group is the version; minimumVersion needs a probe. |
runtimeDependencies |
node and go are host-managed: only id and minimumVersion, discovered with the user's runtime settings; build.invocationPlan returns the probe arguments Core reads their version with, so hosts keep no per-runtime probe. Any other runtime without executables names the tool definition with the same ID, so its id must be a valid tool-definition ID and it declares only minimumVersion: build.invocationPlan returns it with hostManaged: false, version: null, and the single candidate { "kind": "toolchain", "toolchainID": <id>, "root": <root> }, whose resolution checks minimumVersion, and build.checkToolVersion for it is plugin.build.unavailable because Core reads no version. Any other runtime describes its own executables and version probe, so the runtime list is open; a runtime whose only candidate is a toolchain candidate may omit the probe, since the definition checks the version. |
options |
Build dimensions the user chooses: boolean, singleSelect, multiSelect, text, file, directory. Each contributes arguments and environment (with ${value}) when it has a value. Choices are declared, come from the model (choicesFromModel), or are free (allowCustom). |
provider |
The process that answers model and dependency requests when no command prints them; see Build providers. |
model |
static (a toml file read during detection, no process), command (a command whose output Core decodes; it states executesProjectCode and mayUseNetwork), or provider. |
tasks |
The task catalog. scopes say what a task runs on (workspace, project, target); targetTags restrict targets; requiresUserArguments makes the host ask for arguments. A fromModel task is the template for tasks the model lists. |
run |
Run entries from model targets (fromTargets.tags, with arguments) or model tasks (fromTasks, reusing the template task's arguments). |
test |
How tests run: scopes, targetTags, the filters (suite, case) it can select, and results (junitXml or litheTestResults report globs relative to the project). A workspace run reads the globs under the root and every model project. A requested filter must reach the command line through a template whose conditions hold; a case guarded by suiteSelected therefore needs its suite. |
diagnostics |
lineMatcher patterns with capture-group numbers, or litheDiagnostics lines. |
dependencies |
A command (with scopes) whose output Core decodes, as a flat list through a map or as a tree from litheDependencies; or provider (with scopes, workspace and project only). |
sync |
Files whose change makes the model stale, relative to the root and to each project. |
layout |
excluded directories relative to each project, added to every project as excluded content roots. |
An option may declare scope: "instance".
Omitted scope (or "workspace") keeps workspace/build-system storage. Instance
values are independently persisted by normalized detected root: changing or
resetting a cannot change b, even when both accept the same choices. Explicit
false, "", and [] remain stored user values. The host merges only the
matching scope before detection, static run generation, model/dependency/task
requests, synchronization fingerprints, and language import. Instance data cannot
override workspace options, and workspace values cannot silently override an
instance option. When an existing option moves to instance scope, the host copies its shared value
to each detected or previously stored root, preserving explicit instance values
and empty markers. It removes the shared representation only after readback, then
redetects static models. Failure retains the source and blocks publishing defaults
until retry; scope is not a fallback chain. See fixtures/build-systems/build-instance-options.plugin.json.
Each enabled build system gets a tool window of its own, titled with its
displayName; icon (a UI icon) is its activity-bar
icon. As for every tool window, the host owns the shell: dock, order,
visibility, layout, and lifecycle. The host draws the window body from the
model, and three optional fields tell it where an entry goes. None of them
changes a plan.
options[].presentation: "toolbar"puts a boolean option on the window's toolbar as a toggle, for offline mode or skipping tests; it needs anicon."tree"shows a multiSelect option as a checklist node at the top of the tree (Maven profiles). Withoutpresentationthe option appears only in the option list. An icon on an option that is nottoolbarisplugin.manifest.invalid.tasks[].categorylists 1–4 headings, outermost first, that file the task in its project (["Lifecycle"]). Without it the host files the task bygroup. On afromModeltemplate it is the default for the tasks the model lists.- A model task in a
litheBuildModeldocument can carry its owncategory, an array of strings. Core trims the headings. A category that is deeper than four headings or has an empty heading falls back to the template's, so a tool that reports a bad heading still has its task filed.
A host that draws build-system windows gives each detected build system its own tool window and uses these presentation fields. The host-drawn windows share Build Output, docking, layout restoration, activation, and lifecycle with other workbench contributions.
Arguments are template strings, or { "value": …, "when": condition } with
one condition or a list that must all hold: projectSelected,
nestedProjectSelected (a project other than the root one), targetSelected,
suiteSelected, caseSelected, and their no… opposites.
| Variable | Available in |
|---|---|
${root} |
every list |
${project.name}, ${project.path} (relative to the root), ${project.property:<key>} |
lists with a project scope; guard with projectSelected when the project may be absent |
${target.name}, ${target.property:<key>} |
target-scoped tasks, tests, and target run entries; guard with targetSelected when the target may be absent |
${task.name}, ${task.property:<key>} |
fromModel tasks |
${test.suite}, ${test.case}, ${test.case:regex} (regex-escaped) |
test.arguments, guarded by suiteSelected / caseSelected |
${optionArguments}, ${optionArguments:<id>} |
every command list; a whole argument that expands to all options, or one option |
${userArguments} |
tasks and tests; a whole argument. A task with requiresUserArguments must pass it |
${pluginDir}, ${workspaceStateDir}, ${outputFile} |
model and dependency commands; ${outputFile} exactly when output is outputFile |
${value} |
option arguments and environment |
Each option's arguments are emitted once per command line: reference them
either in globalArguments or in the command's list, and only once within a
list, except under opposite conditions. An option that no list references
contributes nothing. Unknown, misplaced, unguarded, or repeated variables are
plugin.template.invalid.
A model or dependency command prints to stdout (default) or to the
host-allocated ${outputFile}. Its decoder is jsonStream
(whitespace-separated JSON values such as NDJSON or go list -json, decoded
as an array; a single document becomes a one-element array) or a canonical
format that needs no map. A static model file is toml.
A map only selects values. A model map turns a decoded document into
projects, each with its targets and content roots, and into option values; a
dependency map turns it into a flat list. Rules select elements with each,
filter them with where predicates ({ "pointer", "equals" | "exists" }),
apply only as a fallback or whenFile a file exists, and read fields with
selectors: "" (the element), @key (its key), /a/b (relative to the
element), $/a/b (from the document root), * (each item). A field is a
selector, { "from", "transform": "lastSegment" }, or a literal
{ "value": "src" }. Target tags are a literal list, and a project's
parent is the deepest project containing it. Paths are relative to the root
or absolute inside the workspace; anything else is dropped. A target fallback is
skipped only when a non-fallback target rule with an overlapping tag already
produced a target, so an explicit binary suppresses an implicit src/main.rs
target while explicit test or example targets do not.
Whatever a map cannot express is the plugin's own work: other file formats,
tasks read from the tool, explicit parent links, tags computed from values,
and dependency trees. The plugin's command (a script under ${pluginDir},
or the tool itself with a plugin-supplied init script, as Gradle does) prints
litheBuildModel or litheDependencies, or a
build provider answers. Unlike a static file, a command
runs only when the host starts it on request. The map does not grow into a
transformation language: a rule is added only when
real plugins need it and it cannot reasonably live in the plugin.
Canonical formats let a plugin's own script print the shape Core expects:
| Format | Shape |
|---|---|
litheBuildModel |
{ "format": "litheBuildModel", "version": 0, "projects", "targets", "tasks", "contentRoots", "optionValues" } with the model's field names; tasks[].category is an array of headings |
litheTestResults |
{ "format": "litheTestResults", "version": 0, "tests": [{ "suite", "case", "status", "message"?, "durationMillis"?, "path"?, "line"? }] } |
litheDependencies |
{ "format": "litheDependencies", "version": 0, "dependencies": [{ "name", "group"?, "version"?, "scope"?, "properties"?, "children" }] } |
litheDiagnostics |
output lines { "lithe": "diagnostic", "path", "line", "column"?, "severity"?, "message" } |
Unknown fields in canonical documents are ignored, so a script may add fields
without a new version. Output that does not fit its decoder is plugin.build.invalidOutput.
A task with source: "provider" is answered by the plugin's build provider.
It keeps the ordinary task ID, scopes, target tags and model selection, but
arguments must be [] and execution must be task. Application/service
launch and test discovery are separate capabilities. Provider model-task
templates cannot be referenced by run[].fromTasks; they need a typed Run
capability rather than an empty command plan. A provider task never
falls back to a host command or a plugin-private function. Command tasks
retain their current shape and omit source.
build.taskPlan returns a provider request instead of argv for this source.
lithe/build/execute receives the effective buildSystemID, workspace-relative
root, options, optionArguments, option environment, task declaration ID,
bounded userArguments, and optional project, target, modelTask selections
(name, workspace-relative path, tags, properties). Scope/path validation
is identical to command tasks. The provider owns build semantics; options
are not a second host-generated command. As with model/dependency requests,
the host may add tool: { executable: <resolved absolute path> } from the
same invocation resolver; explicit invalid tool/runtime selection is a failure,
never permission for independent discovery. It must reject stale project facts
rather than guess a target.
The host offers litheBuild.versions: [0]: the protocol's version is the version
of buildProviders the manifest requires, and the provider answers
{ "version": 0 }. Unsupported negotiation fails explicitly. Task execution and
its progress notification belong to this protocol with lithe/build/model and
lithe/build/dependencies.
Core sets params.operationID to the unique JSON-RPC request ID (as a string);
callers cannot choose it. lithe/build/progress notifications echo that ID and
may include message (up to 4 KiB UTF-8), integer percentage (0–100), and
output (up to 64 KiB UTF-8 per notification). Malformed reports and reports
for ended, cancelled, timed-out or other requests are dropped. Core maps the
wire ID back to the host operation ID; reused host IDs cannot accept old
progress. No plugin-function negotiation is needed.
The reply is { "exitCode": <signed 32-bit integer>, "output": <string> }.
output is the complete bounded diagnostic text (at most 32 MiB UTF-8),
including text streamed in progress; the host displays progress output and
uses final output for the existing diagnostic decoder without duplicating it
in the console. Nonzero exit codes are completed tool failures, not protocol
errors. Malformed replies end with plugin.build.invalidResult; JSON-RPC
errors end with plugin.build.failed. Timeout and cancellation use the
existing request codes and $/cancelRequest. Request completion occurs once;
late replies/progress do not revive it.
The host binds each call to the plugin package, build-system/workspace session, current run, effective option fingerprint and watched configuration revision. Settings, package replacement, disable, workspace close/reopen, cancellation and backend failure invalidate or stop the owning operation. The existing Provider session owns children and writable workspace state; no page, additional runtime, directory or cross-plugin private cache is required. Only public typed results may cross domain boundaries.
Canonical examples: fixtures/build-providers/build-provider-execution.plugin.json and
fixtures/build-providers/build-execution-protocol.json.
A build system's test may declare source: "provider". It keeps the existing scopes, target tags and
supported filters, but arguments must be []. It requires a plugin-owned
Provider process; results may be omitted or use junitXml/litheTestResults
reports: the plugin owns test execution and report production, and the host
reuses the existing report decoder.
build.testPlan returns provider: { method: "lithe/build/test", params }
and the usual results metadata instead of argv. The effective
buildSystemID, root, options, optionArguments, environment, bounded
userArguments, optional project/target, and optional suite/case are
typed fields. A case filter does not require a suite when the provider
supports it; providers must reject selectors they cannot honor rather than
widening the test run. Paths, scopes and declared filters are validated before
the request is sent. The host's report project list only resolves report globs;
it is not a second backend project model.
A provider receives lithe/build/test only for a test that declares source: "provider". Tests use the same Core-assigned operation identity,
lithe/build/progress, tool hint, bounded { exitCode, output } result,
timeout, cancellation and late-result rules as task execution. Reports are
read after execution using the existing freshness, path and size checks.
Workspace-wide reports still require a current project model when the build
system has one. Configuration validity is retained through diagnostic and
report decoding; late results cannot update a changed workspace/configuration.
No new resource/cache location is introduced.
Canonical examples: fixtures/build-providers/build-provider-tests.plugin.json and
fixtures/build-providers/build-test-protocol.json.
Provider tests may additionally declare discovery: true. This requires both
suite and case filters: every discovered selector must be exactly runnable.
build.testDiscoveryPlan accepts the common plan fields and optional refresh
and tool, returning a request for the whole instance:
lithe/build/discoverTests with its effective options. Project/output-file
selection is refused. This method uses
the existing bounded request/cancellation lifecycle, without execution progress.
The provider returns { "format": "litheTestDiscovery", "version": 0, "tests": [{ "suite", "case", "project"?, "target"? }] }. Project and target
are the usual typed selections with workspace-relative paths. There may be at
most 10,000 items and 32 MiB UTF-8 JSON. Unknown document/item fields are
ignored; malformed selectors, invalid paths/scopes, unsupported versions and
oversized lists reject the entire result with plugin.build.invalidOutput.
An empty array is a successful discovery with no tests.
The host passes the reply and original plan configuration to
build.decodeTestDiscovery. Core validates every selector through the same
test-plan rules, deterministically orders the list, collapses exact duplicates,
and derives an opaque id including project/target identity. Equal suite/case
names in sibling modules therefore remain distinct. The returned discovery
contains tests: [{ id, suite, case, project?, target? }]. No source parsing,
framework detection, or second project graph occurs in the host/Core.
Native discovery starts only on a user action, reuses the workspace Provider,
and is independent of plugin pages. Source edits, configuration changes,
package changes and workspace teardown revoke the cached list and in-flight
discovery. A native action must revalidate the selected ID against the current
list before submitting its exact selectors to lithe/build/test; stale
toolbar selections cannot run. The backend must reject a selector whose facts
have become stale externally. Canonical example: fixtures/build-providers/build-test-discovery.json.
A run declaration may use source: "provider". It uses fromTargets.tags,
which is required; fromTasks, nonempty arguments, and extension templates
are refused. The optional name is a literal display name, and execution is
application (default), service or task. workingDirectory is supplied by
the prepared result; the declaration's command-source default has no effect.
These declarations never generate empty command-based persisted Run entries.
build.prepareRunPlan validates a selected run declaration ID, required
project and target selections, effective options, and bounded
userArguments. It returns a provider request for lithe/build/prepareRun,
with the common instance fields, options/option arguments/environment,
run, project, target, and userArguments. An output file is refused.
The host may add the resolved build tool hint. The provider owns upstream
synchronization, build, classpath and launch facts; Core does not reconstruct
those facts from source files or build configuration.
Preparation shares the Core-assigned wire operationID, progress,
timeout, cancellation and stale-result handling of execution. It ends when
preparation finishes; the application is a separate host-owned process.
A JSON-RPC error aborts preparation and cannot launch an application. It retains
the common LSP serverError category; it is not an execution exit-code result
and does not forward a private-function pluginErrorCode.
The successful result is a portable launch plan:
{
"executable": { "kind": "toolchain", "value": "project-jdk" },
"arguments": ["-cp", "classes", "example.Main"],
"workingDirectory": "app",
"environment": {}
}The executable is either a host toolchain identifier (toolchain) or a bare
command resolved by the host (command); absolute paths and shell command
strings are refused. Arguments are separate strings, at most 1,024 entries
and 64 KiB UTF-8 each, without NUL. The environment has at most 64 entries;
keys match [A-Za-z_][A-Za-z0-9_]* with at most 128 bytes, values are strings
of at most 64 KiB without NUL. The complete JSON is bounded at 32 MiB.
workingDirectory must be an existing workspace-relative directory inside
the selected instance, including after resolving filesystem aliases.
Unknown fields are ignored. The host calls build.decodeRunPreparation
with its workspace root, instance root and raw result; any malformed plan
fails as plugin.build.invalidOutput before executable resolution.
The same lithe/build/prepareRun method carries typed launch options. A
provider run may declare execution: "service" or "task"; it uses
fromTargets.tags, never host command or extension templates. Preparation ends
before the host starts the process; the host owns stdout/stderr, process
identity, Stop and cleanup. A provider does not own background services.
The optional launchOptions object contains required execution
(application, service or task), runtimeArguments and arguments
(string arrays), and environment (a string map). Optional entrypoint is an
opaque nonempty string of at most 4,096 UTF-8 bytes without NUL; the owning
provider validates its meaning. Optional workingDirectory is a nonempty
workspace-relative directory, . allowed, without backslashes, absolute
paths, empty components or ... Unknown option fields are refused. Argument
and environment bounds match the launch result above, including empty argv
values. Options describe resolved user choices; providers must preserve
argument boundaries and cannot silently ignore unsupported choices.
execution must match the run declaration (default application). Service
and task preparation require launchOptions; application preparation without
it uses the declaration's defaults. Both SDKs reject malformed options before
invoking a handler.
Build options, resolved tool hints and native settings remain their existing
sources; launch options do not create a second profile or tool configuration.
The host supplies workspaceRoot to build.prepareRunPlan when options contain
an explicit working directory. Core validates it against the canonical workspace,
including aliases, before preparation. The host passes the same options to
build.decodeRunPreparation; the result must use exactly that directory.
Only this explicit override may lie outside the selected instance. Without
an override the original instance containment rule still applies. Other
launch result validation and cancellation/stale-result behavior are unchanged.
Canonical fixtures: fixtures/build-providers/build-provider-run-options.plugin.json and
fixtures/build-providers/build-run-options.json.
A provider service run may declare
debug: {"configurationType":"example"}. This is a public DebugAdapter Registry
alias, not another plugin's private language server or a local adapter ID.
The host selects one currently enabled adapter advertising the type and attach,
respecting an explicit qualified adapter selection. Missing or ambiguous entries
fail before starting the provider process. It sends:
{"debug":{"adapterID":"dev.example/debug","configurationType":"example",
"target":{"kind":"connection","host":"127.0.0.1","port":51235}}}These are the exact three debug fields and three target fields. The host owns the port (integer 1–65535), process, readiness and cleanup. Explicit null, unknown fields, remote hosts and application/task execution are rejected. The type must match the selected run declaration. The provider uses its upstream engine to suspend the service until attachment; it owns runtime-specific flags and fails if it cannot honor the request. It returns the ordinary portable Run process plan, without selecting an endpoint or returning DAP arguments.
The host retains the adapter's qualified identity, package and registry generation
through preparation, readiness and attach. Disable, replacement or stale input
revokes the operation and cleans its owned process. Adapter configuration is
produced by existing debug.adapters.startArguments with a connection target;
the selected registered adapter owns its transport. In particular,
tcpLanguageServer may refer only to a language server declared by that adapter's
own plugin. No built-in language debugger fallback is permitted.
This does not change same-package native prepareDebug.
Canonical fixture: fixtures/build-providers/build-connection-debug-options.json.
A provider run may additionally declare debug: {"adapter":"native"}. adapter is a
local ID declared by this plugin; qualified cross-package references are refused.
The adapter must serve launch of executable targets, with bindings program ←
target.path, cwd ← target.workingDirectory, args ← target.arguments,
and env ← target.environment. These fields cannot be overridden by adapter
default arguments. It covers native executable launch only;
it does not accept attach endpoints, backend-selected adapters or arbitrary DAP JSON.
build.prepareDebugPlan validates the same selected run/project/target and
arguments as run preparation and returns lithe/build/prepareDebug. Progress, cancellation, timeouts, tool hints, workspace
ownership and JSON-RPC failure handling follow run preparation. A successful result
contains exactly these four fields:
{"program":"app/bin/sample","arguments":["a b","x#y"],"workingDirectory":"app","environment":{}}The provider owns synchronization/build before returning. program is an existing
workspace-relative regular file contained in the selected instance, including
canonical symlink resolution. Arguments, environment and directory retain run
preparation's size and containment limits. Unknown fields are refused here; there
is no adapter process, socket, shell, private function or DAP argument channel.
build.decodeDebugPreparation takes the selected run separately and returns
preparation with its validated root, declaration-owned adapter, and these
four fields. Missing, malformed and escaping targets fail before adapter startup.
The native host saves documents before preparation, retains model/configuration/
source/plugin identity through decode, and drops stale results. It checks executable
permissions and canonical containment again at handoff. debug.adapters.startArguments
binds the validated target facts; the existing Registry launches the adapter using
its own permissions and package process declaration. The launch configuration retains
pluginID/adapterID for routing and retry. Stopping preparation cancels only its
operation; the Debug host owns launched sessions and plugin withdrawal cleanup.
Saved Run configurations keep their existing selectors and prepare again for Debug.
Fixtures: native declaration and native result. This does not provide preparation through a language server, long-running services or cross-plugin collaboration.
The native Build tree re-resolves model IDs before preparation. Source/model,
option, settings, package and workspace changes revoke a pending preparation.
After validation the existing Run host resolves the executable and owns output,
stop, exit status, and argument-resource cleanup. Workspace teardown and plugin
disable stop the application by its exact host operation ID; an old owner
cannot stop a replacement process. The Run host can explicitly save a selector to the existing local Run v2 layer
as provider: "build.provider" with extensions.build. The reference contains
pluginID, buildSystemID, instance root, run, projectID, targetID, and
raw userArguments, never an executable, environment, or cached classpath.
Every restart re-resolves the enabled instance/model and sends a fresh preparation.
Missing plugins/targets/models fail explicitly; there is no command fallback.
Saving and running work before detector-generated Run configurations exist.
See fixtures/build-providers/build-provider-run-preparation.plugin.json
and fixtures/build-providers/build-run-preparation.json.
An own-process provider may opt into buildSystems[].configuration: {"source":"provider"}.
Shared language-server providers cannot opt in.
build.configurationPlan uses the same selected instance/options/environment/tool
snapshot as model loading and emits lithe/build/configuration. It has no project,
output file, or mutation parameters. A provider that implements the method
advertises the server feature litheBuildConfiguration: true. Configuration reads are bounded requests
with the existing cancellation, timeout, workspace and package ownership.
build.decodeConfiguration validates result and returns configuration:
{"format":"litheBuildConfiguration","version":0,"sources":[...]}. There are at most
8 sources, each with a unique provider-owned id, scope (installation, user,
workspace), file uri, exact byte revision (missing or sha256: plus 64 lower-case
hex digits), writable, and up to 64 fields.
Installation sources are always read-only. Two scopes may alias one URI only when
both are read-only and have identical revision and fields. File URIs
cannot carry credentials, queries or fragments.
Each field contains id, kind (text, file, directory, boolean, stringList),
value (raw native value, or null for absent), effectiveValue, effectiveSource
(an upstream technical origin ID). IDs are opaque bindings interpreted by the
provider; there is no document-format, XPath, XML root or element binding in the
host contract. Fields cannot duplicate or expose credential/secret fields;
values and identifiers are bounded and kind-checked. Full native documents, secret
values, credential-bearing URLs and private keys must never enter this response,
logs or normal controls. Source discovery, interpolation, precedence and effective
values belong to the selected upstream engine; Core does not infer them.
The native host populates raw/inherited values, displays scope and target URI,
maintains drafts, and requires explicit Save. Opening, cancelling, losing focus
and leaving a page do not write native sources. Passive page reads refuse a
project-owned executable (such as a cold Wrapper), and cannot silently delegate
failed automatic tool discovery to the Provider. An explicit Read using project
tool action authorizes that tooling; explicit Save also owns its required read-back. The confirmed edit is
{sourceID, revision, values:{<fieldID>:<new raw value or null>}}; null removes only
that native override. Unchanged inherited values must not create overrides.
build.configurationEditPlan takes the same instance/tool/options context,
snapshot, confirmed edits, and a fresh, empty, already existing outputFile
inside the host's isolated workspace state. It validates the bounded field IDs,
types and revision, then plans lithe/build/editConfiguration.
The provider owns its native format and uses an upstream/mature reader/writer;
Maven XML, JSON and TOML do not add host format cases. The provider verifies the
source revision and writes replacement bytes only to that private staging file.
Open without CREATE or following a symlink, validate that it is the empty staging
file before writing, and never truncate another file. A cancelled operation cannot
recreate removed staging.
Preserve unrelated content, format-supported comments and secrets privately.
The reply is only {format:"litheConfigurationEdit", version:0, sourceID, revision, changed}. build.decodeConfigurationEdit binds this receipt to the
same snapshot/draft and rejects extra fields. Full documents and private bytes
must never appear in RPC, UI, logs or model inputs. The native adapter validates
the staged bytes against the receipt SHA-256 before publication. A no-op creates
no source or native override. Providers never write the final source through
this workflow; publication remains the host's guarded atomic transaction.
The output uses the existing private outputs lifecycle, is never reusable across
worktrees, and is deleted on success, failure, cancellation, workspace/window
close and plugin revocation. A late backend may finish its already-open file
handle, but cannot resurrect a removed file. Installation resources stay read-only.
The platform adapter denies app/plugin installation roots, validates the selected file and canonical target, preserves metadata and performs guarded atomic publication against the original bytes. Before commit the operation must still own its workspace, package, settings and monotonic option revision. Conflicts retain the draft. After save the host invalidates build/test/language import facts and re-queries the owning provider; read-back must identify the saved source and exact published revision. A failed read-back is not a successful effective-value publication. Native probes, output files and mutable provider caches live only in workspace/platform state and are not cross-worktree reusable. Installed packages remain byte-for-byte unchanged.
Fixtures: declaration and native facts.
A producer opts into buildSystems[].modelSharing: { options: [<optionID>] }.
Only normalized projects, targets and contentRoots, including their
ordinary public metadata, are published. Tasks, run entries, settings,
optionValues, process handles and private functions are excluded. The
options list names existing ordinary build options whose effective values
may be shared; omitted options stay private. A configuration fingerprint
identifies all effective options without publishing their values.
A consumer declares modelInputs: [{ pluginID, buildSystemID }], requires
buildModelInputs: 0 and buildProviders: 0, and owns a provider and model.
Slots are exact optional identities; Core sorts them by plugin ID and build
system ID and rejects duplicates. A producer cannot also consume
inputs, so consumers cannot recursively re-export a model.
The native host calls build.resolveModelInputs with the consumer manifest
and {revision, sources: [{manifest, instances: [{buildSystemID, root, state, model?, options?}]}]}. Sources are enabled installed packages in the current
workspace activation, never identities supplied by another backend.
Input states are ready, loading, stale, or failed; only ready
instances include a complete normalized model. A truncated model becomes stale.
Core revalidates source manifests, ownership, shape and path containment and
returns modelInputs: {version: 0, revision, inputs}, where each input is:
{
"pluginID": "dev.example.source",
"buildSystemID": "source",
"state": "available",
"instances": [{
"root": "app",
"state": "ready",
"model": {"projects": [], "targets": [], "contentRoots": []},
"configurationFingerprint": "<64 lowercase hex characters>",
"options": {}
}]
}A missing, disabled, non-public or mismatched producer gives
state: "unavailable", instances: []. An installed public producer with no
detected instance is available with an empty list. A non-ready instance has
only root and state; no old model or options travel with it. Roots and
model paths remain workspace-relative, and instances are deterministically
ordered. There are at most 8 slots, 64 instances per source, 2,000 projects and
10,000 targets/content roots per instance, and 8 MiB of resolved JSON. The
revision is a nonnegative integer no larger than 9,007,199,254,740,991.
The host supplies this snapshot as the common plan request's optional
modelInputs. It is required exactly for a consumer with declared slots.
Every Provider plan copies it unchanged into params.modelInputs; Core
revalidates the snapshot and rejects extra fields or mismatched slots.
The running provider must have negotiated litheBuild; a session that has not
rejects an input-bearing request even for an existing method such as
lithe/build/model.
A revision belongs to one workspace activation. The host revokes consumer model/dependency loads, discovery and in-flight execution/preparation when a producer's model, options, watched configuration or package availability changes. Revisions also detect changing an option away and back before a reply arrives. Guard the result after decoding as well as after the RPC; do not publish an old reply merely because cancellation was ignored. Already launched applications retain their existing native ownership.
This facility never installs or activates a producer, invokes its private
functions, grants permissions, leases a process or resolves a package version.
There is no general plugin-dependency/export API. Backend authors must handle
unavailable and non-ready slots explicitly. Language integration remains
separate work. Shared fixtures:
fixtures/build-systems/build-model-producer.plugin.json and
fixtures/build-systems/build-model-consumer.plugin.json.
A provider test may declare results: { "decoder": "provider" }. Core adds
testResults: true to the test request. The reply includes
testResults: { "format": "litheTestResults", "version": 0, "tests": [...] }
next to exitCode and output. Ordinary execute replies may include the same
report when the task also ran tests. Missing required or malformed reports fail
explicitly; there is no host report-file fallback.
Each case has suite, case, status (passed, failed, error, skipped),
optional workspace-relative project and source path, one-based line,
durationMillis, message, and invocations (default 1). A report contains at
most 10,000 cases and 8 MiB UTF-8 JSON. Paths remain inside the selected build
root, durations are nonnegative safe integers, and messages are bounded. Core
validates and aggregates by project/suite/case, so identical names in distinct
projects remain distinct; it computes summary/success from the cases. Repeated
case durations are summed only if every invocation has a duration and the total
is a safe integer; otherwise the aggregate duration is omitted.
An optional coverage: [{ "project": "module", "suite"?: "Suite", "case"?: "method" }]
records completed scopes. A missing case covers the entire suite; a missing suite
covers the project (and forbids case). Scopes use canonical workspace-relative
projects inside the instance root, at most 10,000 entries, and bounded nonempty
suite/case names. Core sorts and deduplicates them. Hosts may replace old outcomes
within those scopes; without coverage they replace only the returned exact cases.
Providers must not claim a complete suite when upstream filters or interruption
could have limited execution. Opaque selectors are never parsed by hosts to infer
coverage.
The provider owns report discovery, freshness and upstream format semantics.
It must preserve reports from failing executions and reject unreadable/oversized
reports instead of returning empty success. The host passes the reply to
build.decodeTestResults as request.result with the same run/configuration
identity, checks ownership again after decode, and only presents the result.
Cancellation or configuration changes revoke late reports just like late output.
The report is operation state in a platform-provided writable directory, never
installed package state or a cross-worktree cache. Fixture:
fixtures/build-providers/build-provider-reports.plugin.json.
Provider test declarations may advertise selectorKinds, up to 16 distinct public
namespace identifiers (120 bytes each). A test
request may then carry selectorKind and selectors instead of suite/case.
The namespace must be declared; at most 64 nonempty, control-free UTF-8 values of
4096 bytes each are allowed. An empty list explicitly selects all tests within the
request's normal project/workspace scope. Values remain opaque to the host and
Core: only the owning language engine supplies semantic identities and the build
backend translates them into its upstream runner's selection syntax. Providers
must reject identities they cannot represent exactly, rather than widen execution.
Ignoring a filter is unsafe, so Core rejects these fields before dispatch
unless the test declares the namespace. The operation identity, source/model
and configuration validity, cancellation, progress, and inline report rules
still apply. The shared example is fixtures/build-providers/build-provider-selectors.plugin.json.
A project task may declare preparation: { "purpose": "run", "sourceRoles": ["source", "generatedSource"] }. Purpose is
an opaque workflow identifier; run identifies preparation before resolving a
language launch. Roles are the canonical model content-root kinds. A provider
can declare a different task for testSource/generatedTestSource, with any
private task IDs. Hosts never prescribe task names or infer a role from a file
extension, build descriptor, class name or missing model entry.
Bindings are unique by purpose/role within each build system. Preparation
requires a project-scoped task, task execution, no argument prompt, and no
fromModel. Empty, overlapping, duplicate or unknown roles are rejected.
Hosts select the deepest owning project and its deepest content root from the
current model, then require exactly one declared matching task. Missing or
ambiguous declarations fail; they do not fall back to a conventional task ID.
The selected ID travels through the existing execute plan and backend method.
Shared fixture:
fixtures/build-providers/build-provider-preparation.plugin.json.
The build backend owns filtering, custom resource directories, profiles and reactor semantics. Hosts await successful completion, retain one operation identity through cancellation/configuration changes, and only then resolve and launch the program. Per-run option/tool overrides are ephemeral host inputs to the existing validated plan; they never mutate saved build defaults or claim that the persistent model reflects a different configuration.
Some tools expose their model only through their own API (the Gradle Tooling
API, Mix inside the BEAM). Their plugin declares provider: a long-lived LSP
base-protocol process of the same kind as a language server, which answers
lithe/build/model and lithe/build/dependencies. It has one of two forms,
told apart by the languageServer key; mixing them is
plugin.manifest.invalid.
| Form | Fields |
|---|---|
| Shared | { "languageServer": "<languageID>", "executesProjectCode", "mayUseNetwork" }. The named language must have a language server in the same plugin; the host sends the requests to that session. |
| Own process | The language-server process fields (launch, arguments, validationArguments, runtimeDependencies, initializationOptions) plus executesProjectCode and mayUseNetwork. Validation, launch templates, settings, and the install review are those of a language server, so it needs the languageServer.launch permission and is listed as a packagedBuildProvider, systemBuildProvider, or runtimeBuildProvider program with its buildSystemID and flags. |
model.source: "provider" and dependencies.source: "provider" use it. A
provider no source uses, or a provider source without a provider, is
plugin.manifest.invalid. Detection never starts a provider: its instances
report modelSource: "provider" without a model.
The host starts the process from plugins.declarative.languageServerLaunchPlan
with buildSystemID. For a provider source, build.modelPlan and
build.dependencyPlan return provider: { method, params } instead of
arguments, with the request's decoder (litheBuildModel or
litheDependencies) and the declared executesProjectCode and
mayUseNetwork:
| Param | Meaning |
|---|---|
buildSystemID, root |
The instance. A model request always covers the whole instance. |
options |
Effective option values by option ID. |
optionArguments, environment |
Arguments from options, and selected runtime/tool environment merged with option contributions (options win). The host does not forward its complete environment. Providers use this environment for every invocation and cache identity. |
project |
Dependencies only: { name, path } of the selected project, or null for the workspace. |
refresh |
Optional; true when the user asked to reload (Reload, Retry, a page's build.refresh) or the host reloads after a sync file changed. The provider must then recompute instead of answering from a cache or snapshot of its own. Absent or false for a first load, which a provider may answer from a snapshot it still trusts. |
tool |
Optional; { executable }, the absolute path of the build tool the host resolved from the build system's invocation the same way a task run does (project wrapper, environment variable, PATH, the user's override), so the provider need not repeat the lookup. Absent when the host could not resolve it; the provider then looks the tool up itself. An explicit toolchain or path selection that cannot resolve fails before the Provider request; it cannot be dropped to trigger independent lookup. |
The Core-only tool hint may include a bounded environment string map. Core merges it into the existing Provider environment parameter, with option contributions taking precedence; the wire tool remains { executable }. This also applies to native configuration, tasks and tests. The backend process runtime and the selected build runtime are independent.
The host passes refresh and tool in the build.modelPlan and
build.dependencyPlan requests; Core validates tool.executable as an
absolute path (plugin.build.invalidSelection otherwise) and copies both into
params only when they are set. Command sources ignore both.
The host passes the JSON result of the request as result to
build.decodeModel or build.decodeDependencies, which decode it like the
canonical format of a command; a missing result is
plugin.build.invalidSelection, and a result over the output limit or of
another format is plugin.build.invalidOutput.
The manifest only says how to start the provider and which sources use it.
Everything else is negotiated in the LSP initialize exchange, so a provider
can add optional data:
- The host sends
capabilities.experimental.litheBuild: { "versions": [0] }. The protocol has no version of its own: it is the version ofbuildProvidersthe manifest requires. It is the two requests above with the params and result formats of this section, together with the typed requests of the sections before it: task and test execution, test discovery, run preparation, native debug preparation, source-backed native configuration, inline test reports, opaque semantic test selectors, typed run options, and registry connection debugging. Read-only model inputs travel in the plan requests. A host presents the features it implements; a provider must not depend on a host calling any of them. - The provider answers
capabilities.experimental.litheBuild: { "version": 0 }with the version it chose from that list. A provider that answers no version, or one the host did not offer, is not used for build requests; a shared provider still serves its language. - Both sides ignore fields they do not know in these objects and in request
params and results, so either side can add optional data. A change the other
side must not ignore gives
buildProvidersversion1, and the protocol moves with it.
A host that declares buildProviders keeps providers apart:
- One process per plugin, build system, and workspace. Two plugins never share a process, and the host never relays messages between processes; each speaks JSON-RPC only with the host, over its own stdio.
- The shared form reuses a language server session of the same plugin only.
- A provider starts on the first build request, which only a user action sends, and stops when its workspace closes, its plugin is disabled, or before its package is replaced, rolled back, or uninstalled. A change of the plugin's settings restarts it on the next request, so its launch arguments always reflect the current values. A provider that exits is started again by the next request.
- Installation refuses a package whose provider launch points at a missing file, a path outside the package or through a symbolic link, or a packaged executable or runtime that is not a regular executable file.
- Its writable state lives in the plugin's state directory for that build system, never in the plugin package or the host's installation.
hostIntegration is reserved. Core implements no build-system host
integration: every build tool is described by its plugin's data and backend,
and no host code branches on a build-system ID. Any hostIntegration name is
refused with plugin.integration.unsupported at
buildSystems[<n>].hostIntegration. Model, test-result, and dependency sources
have no hostIntegration kind: build systems use the declarative contract, a
canonical format, or a provider.
The build.* Core commands (see rust/contracts/rust-core-api.md) take the
manifest, the build-system ID, and a request, and revalidate the manifest
every time. build.detect walks the workspace once for all enabled plugins
and returns instances, with the model of static sources. build.invocationPlan and build.checkToolVersion resolve the
executable; build.resolveOptions checks stored option values and returns
the configuration fingerprint; build.modelPlan, build.taskPlan,
build.testPlan, and build.dependencyPlan return argv plans, or provider
requests for a provider source;
build.decodeModel, build.decodeTestResults, build.decodeDiagnostics,
and build.decodeDependencies normalize output or a provider result; build.syncFingerprint
digests the watched files. A selection the declaration does not allow is
plugin.build.invalidSelection; an operation it does not declare is
plugin.build.unavailable.
vcsProviders[] declares version control systems the plugin implements, each
through its own LSP base-protocol process: the VCS ID, the static markers that
identify a root, metadata paths the host watches, the VCS client the provider
drives, and the shared process launch
fields. The ID git is reserved for the built-in support
(plugin.vcs.reserved). Capabilities belong to the running process and are
negotiated in initialize under experimental.litheVcs, never declared in
the manifest. The host offers { "versions": [0] }: the protocol's version is the
version of vcsProviders.
Detection never starts a process: vcs.detectRoots finds roots from the
markers, and the host starts a provider only after a root of its VCS is
found. The install review lists the provider as a …VcsProvider program with
its VCS client (vcsTool) and mayUseNetwork. Requests go through
lsp.vcsRequest by operation name and replies are normalized by
vcs.decodeReply. The fields, operations, reply shapes, and error codes are
defined in the VCS provider contract.
debugAdapters[] declares debug adapters the plugin implements. Each one is a
DAP process the host starts and talks to over stdio or a loopback TCP port, or
a debug server hosted by the plugin's own language server.
The declaration is data only: there is no resolver, workflow, host integration,
or initializationOptions, and any such field is refused. It requires
debugAdapters: 0 in requires, debug.launch (plus debug.attach when an
adapter serves attach), and one onDebug:<id> activation event per adapter.
| Field | Meaning |
|---|---|
id, displayName |
Plugin-local ID (unique in the plugin) and its title. |
adapterID |
DAP initialize.adapterID; defaults to id. |
requests |
launch (default) and/or attach. |
selectors[] |
Optional automatic matching by targetKinds and languageIDs; absent means explicit selection only. |
configurationTypes |
Imported launch.json type values the adapter answers to. |
configurations.launch/attach |
Default DAP arguments, a packaged parametersSchema, and targetBindings (program ← target.path, processId ← target.processID, cwd ← target.workingDirectory, and for attach only connection ← target.host, target.port, target.pathMappings; also bounded args ← target.arguments and env ← target.environment; see Remote attach). A configuration for an undeclared request is refused. |
configurations.launch.executable |
For a target that is a source file the adapter cannot debug directly (C, C++, Swift): setting names a file setting of the plugin, candidates[] (up to 8) are the paths a host looks for when the user chose nothing. Each candidate starts with ${workspaceFolder}/ or ${fileDirectory}/, may use ${fileBasenameNoExtension}, and has no ... Declare it together with targetBindings.program: "target.executable" and neither without the other; a launch fills program with the path the host settled on. The user's setting always wins: a chosen path that is not a runnable file is an error, not a reason to try the candidates. The host shows the path it found in that setting, and the setting's description is where the plugin teaches how to produce the program. |
capabilities.disabled |
Standard DAP supports* names or exceptionBreakpointFilters the host treats as absent. |
features |
Adapter-specific DAP extensions the adapter implements; see Adapter features and defaults. |
defaults |
Values the adapter starts with when the user configured none. |
console.commandPrefix |
Text (1–8 characters, no spaces; for lldb-dap a backtick) the adapter's console reads as "what follows is a debugger command". The host puts it before the typed line only when it asks the adapter to complete, and moves the adapter's answer back, so the user never sees it. An adapter that treats a bare word as an expression (lldb-dap) otherwise mixes language keywords such as long into the first word's completions; the prefix limits them to commands. Declare at least this or console.commands[] when console is present. |
console.commands[] |
Debugger commands (name, optional one-line description) the Debug Console offers as completions. An adapter that answers the DAP completions request completes live, so list commands only for an adapter that does not (or whose live answers lack some commands); the host merges both. Names are inserted as written, unique, and not padded with spaces; descriptions are shown as written and not translated. |
launch.process |
The shared process launch fields without initializationOptions, or {"launch": {"kind": "runSelectedRuntime", "tool": "<tool-definition ID>"}}, which starts the adapter with the executable the user's runtime selection resolves for that tool; the ID is listed in requiresTools (see Tool references). Required for every transport except tcpLanguageServer, which must not have it. |
launch.transport |
stdio, tcpAnnounced (endpoint: stream, one-line prefix, value = port or hostPort), tcpPort (endpoint.port), or tcpLanguageServer (endpoint: languageID, command). TCP endpoints are loopback only. |
launch.endpoint for tcpLanguageServer |
For an adapter that lives inside a language server and has no process of its own (the Java Debug Server runs in JDT LS). languageID names a languageServers[] entry of the same plugin; command (1–128 characters of letters, digits, ., _, :, -) is the workspace/executeCommand the host sends to that server for each session. The server answers with the loopback port to connect to, as an integer or a numeric string from 1 to 65535. The host starts the language server for the workspace first, as it does for editing. The command is part of what the user reviews at install and update, and changing it asks again. |
A host never recognizes an adapter by its ID. What is specific to an adapter is declared, and the shared debug engine follows the declaration:
"features": {
"hotCodeReplace": {
"request": "script.reload",
"event": "script.reloaded",
"changedField": "modules",
"errorField": "error"
},
"stepFilters": { "argument": "stepFilters" }
},
"defaults": {
"stepFilters": { "classNameFilters": ["_vendor.*"], "skipSynthetics": true }
}| Declaration | Meaning |
|---|---|
features.hotCodeReplace.request |
The DAP command the host sends to apply changed code to the running debuggee (the standard redefineClasses inspection). Present means the adapter supports hot code replace; absent means it does not. |
features.hotCodeReplace.event |
A DAP event whose message the Debug Console shows as progress. |
features.hotCodeReplace.changedField, errorField |
Fields of the response body: the list of replaced units, and a failure message the adapter reports in a successful response. A response without changedField replaces nothing visible. |
features.stepFilters.argument |
The launch argument through which the adapter receives stepping filters, shaped {skipClasses, skipSynthetics, skipStaticInitializers, skipConstructors}. An argument the user's configuration already sets is kept. |
defaults.stepFilters |
classNameFilters (up to 256 single-line patterns of at most 256 characters), skipSynthetics, skipStaticInitializers, skipConstructors, hideFilteredStackFrames. Used when neither the launch nor the user's settings name filters. |
Names (request, event, fields, argument) are a letter followed by up to
63 letters, digits, ., _, or -. A declaration with a malformed name, or a
field the contract does not define, is plugin.manifest.invalid with the
field that names it. The launch plan's debugAdapter descriptor carries
both so the host can pass them to debug.createSession.
An adapter that attaches to a debuggee on another machine (a JVM started with
-agentlib:jdwp, a Node inspector, gdbserver) is an
ordinary attach adapter whose targetBindings carry a connection. The
adapter itself still runs on this machine, stdio or loopback as before; only
the debuggee is remote. This is the adapter-to-debuggee endpoint, not the
IDE-to-adapter DAP endpoint. By default, debugpy --listen starts a DAP adapter
on the target machine: the IDE must connect to that adapter, not start another
local adapter and merely pass it connect.host and connect.port.
connection does not declare an external DAP endpoint or remote adapter
execution (for example ssh host gdb -i=dap). These require separate host
integration; this contract and its validation do not establish that support.
The current transport scope is ordinary TCP connection and lifecycle, reusing
the existing socket implementation and retaining stdio. It does not add proxy,
tunnel, port-forwarding or SSH execution settings, or relax the loopback rule
for language-server-provided DAP endpoints. Adapter-native source mapping should
be reused where applicable, without translating the same path pair again in Core.
The following illustrates nested argument binding and mapping shape, not a
complete debugpy --listen connection configuration:
"configurations": {
"attach": {
"arguments": { "timeout": 5000 },
"targetBindings": {
"connection": {
"host": "connect.host",
"port": "connect.port",
"pathMappings": { "shape": "objects", "argument": "pathMappings",
"localKey": "localRoot", "remoteKey": "remoteRoot" }
}
}
}
}- The values of
connectionare paths into the adapter'sarguments, because adapters name and nest them differently: java-debug takeshostNameandport, nested arguments may useconnect.hostandconnect.port, js-debugaddressandport, gdb onehost:portstring. A path is 1–4 dot-separated names of letters, digits,_, and-(up to 64 characters each) and Core creates the intermediate objects. hostandportare declared together.addresstakes the endpoint as onehost:portstring, with an IPv6 host in brackets ([2001:db8::1]:2345); it stands in forhostandport, or is a further destination beside them. At least one of the two forms is required.pathMappingsis optional and has four shapes, one per way adapters take source roots:objectsfills a list of objects, one per mapping:argumentnames the list,localKeyandremoteKeythe keys of each object ([{ "localRoot": …, "remoteRoot": … }]).firstis for an adapter with a single pair of roots and fillslocalandremotefrom the one mapping. It takes exactly one: a target with more is refused rather than truncated.dictionaryfills an object atargumentwhose keys are the roots of one side and whose values are the other ("keyedBy": "remote"gives{ "<remote>": "<local>" }). A target with the same root twice on the keyed side is refused, since an object holds one value per key.pairsfills a list of two-element arrays atargument, in the order"order": "remoteLocal"([["<remote>", "<local>"]]) or"localRemote". An adapter whose mapping is none of these declares nopathMappings, and a start that sends mappings to it is refused.
- A destination may not equal, contain, or sit inside another destination, the
processIdorcwdargument a declared binding of the same configuration fills, or a name that the configuration's ownargumentsalready sets. Defaults win over bindings, so without this rule the adapter would connect somewhere other than the endpoint the user confirmed. A manifest that does it isplugin.manifest.invalid. connectionis allowed underattachonly.selectors[].targetKindsmay listconnection, and only for an adapter that declares the binding.
The target of a start is { "kind": "connection", "host", "port", "pathMappings"?: [{ "local", "remote" }] }. host is a DNS name or an IP
literal of 1–253 characters, with labels of up to 63 letters, digits, and
hyphens, none leading or trailing: no scheme, port, brackets, whitespace, or
user. A dotted run of digits must be a valid IPv4 address, and IPv6 zone IDs and
internationalized names (use punycode) are not accepted. port is 1–65535.
There are at most 16 mappings; each root is 1–1024 characters without control
characters, and a local root must be an absolute path on the host's
platform.
debug.adapters.startArguments answers with the filled arguments and a
connection: { host, port, remote }. remote is false only for what
Core can prove local from the text: localhost (any case), 127.0.0.0/8, and
::1. Every other host, including 0.0.0.0, ::ffff:127.0.0.1, and
localhost.example.org, is remote: true. A host asks the user to confirm each
remote endpoint it has not been told to trust, and does not remember the
answer by default. debug.attach covers remote attach; there is no separate
permission. plan.debugAdapter.remoteAttach in the launch plan is
{ "pathMappings": boolean, "maxPathMappings": number } for an adapter that
takes a connection (maxPathMappings is 0 without mappings, 1 for first, and
16 otherwise) and null otherwise, so a host knows whether to offer the
host-and-port form and the mapping list, and how many rows the list may hold.
The cases in fixtures/debug-adapters/debug-adapter-rejections.json state the stable code and
field for each refusal and the layer that reports it: schema,
coreManifest, corePlan (one start: stale registration or runtime
selection, a target missing a fact a binding reads; the request defaults to
launch and context.request sets attach), and coreInstall
(a parametersSchema that is missing or links outside the package).
fixtures/debug-adapters/debug-adapter.plugin.json is a valid example, and
fixtures/debug-adapters/debug-adapter-remote-attach.plugin.json declares six adapters that
differ in how they take a connection.
outputLinkMatchers[] lets a plugin make text in the Run and Test consoles
clickable, such as a traceback frame or a panic location. The host knows no
tool's output format: the plugin declares the pattern and Core finds the
matches. Requires outputLinkMatchers: 0 in requires. Matchers alone are a
complete plugin: one that declares nothing else is valid, and matchers without
resolve need no backend.
"outputLinkMatchers": [
{ "id": "traceback", "pattern": "File \"(?P<path>[^\"]+)\", line (?P<line>\\d+)", "scope": "run" }
]| Field | Meaning |
|---|---|
id |
Plugin-local ID: a lowercase letter followed by up to 63 lowercase letters, digits, ., _, or -; unique in the plugin. At most 16 matchers per plugin. |
pattern |
A regular expression of 1 to 512 bytes matched against one console line. It uses the Rust regex syntax, so it has no backreferences and no look-around, and its compiled size is limited. It names at most 8 groups, each a camelCase name, and must not match empty text. |
scope |
run or test: the console the matcher applies to. Absent means both. |
resolve.function |
Optional ID of a function of the plugin's backend process. Needs backend. |
Without resolve, the pattern must capture a path group and may capture
line and column (1-based); no other group name is accepted. A relative
path becomes one candidate per workspace root, an absolute one counts only
inside a workspace root, and a path that leaves every root is not linked: a
console line can never open a file outside the workspace. With resolve, the
pattern may name any groups. The host calls the function when the user
clicks, with {groups} (the captured text by name) and the window's context,
and the backend answers {uri, range?} or null.
Core owns the decision, so every host links the same text:
outputLinks.matchtakes{scope, platform, caseSensitive?, workspaceRoots, sources: [{pluginID, matchers}], lines}and returns{links}. Each link is{line, start, length, pluginID, matcherID}plus eitherlocation({paths, line?, column?}: the host opens the first existing path) orresolve({function, groups}).startandlengthcount UTF-16 code units of the line. Matches are found in source order and then declaration order, and a match overlapping an earlier link is skipped. A request carries at most 256 lines, 64 sources and 16 roots; a line is scanned up to 4096 bytes and links at most 8 times.outputLinks.decodeResolutiontakes{reply}(what the function answered) and returns the location to open,{path, line?, column?}with 1-based positions, ornull. A reply opens only afile:URI and only the membersuriandrangeare accepted; a 0-basedrange.startis converted.
The host runs matching only for lines it displays, never for output a plugin
has not been enabled for, and ignores a link whose file does not exist. Fixtures:
fixtures/run/output-link-matchers.json and fixtures/run/tool-reference-launch.plugin.json.
A plugin with its own pages may need code that a page cannot run: database
drivers, network clients, file formats. backend declares one long-lived
process that serves only the plugin itself, independent of any build system,
language, or workspace:
"pluginAPIVersion": 0,
"requires": { "backend": 0, "ui": 0 },
"permissions": ["backend.launch", "credentials.store", "ui.contribute", "ui.web"],
"backend": { "launch": { "kind": "packaged", "path": "bin/backend" }, "arguments": ["--state", "${stateDir}"] }backendholds exactly the process launch fields, with the same templates and install review; the review lists it aspackagedBackend,systemCommandBackend, orruntimeBackend. It needsrequires.backend: 0and thebackend.launchpermission.fixtures/backend/backend.plugin.jsonis the example; the backend cases offixtures/manifest/invalid-manifests.jsonare the rejections.- It belongs to no workspace:
${workspaceStateDir}is refused, and its writable state is${stateDir}. The host starts it with${stateDir}as root and working directory. - The host keeps at most one backend process per plugin, starts it on the first trigger listed under Backend lifecycle and workspace contexts, and ends it when the plugin is disabled, updated, or uninstalled, and when the host quits. Its consent is the install review: it needs no per-workspace prompt.
- The backend speaks the plugin-function protocol and may
ask the host for the host services its permissions grant.
It owns no domain contract: it never answers
lithe/build/*or VCS requests. - A plugin publishes one archive per platform, so its packaged path does not
vary by
${arch}; a macOS archive may carry a universal binary.
A backend may start without a page and learns about workspaces only through
contexts. fixtures/backend/backend-workspace.plugin.json is the example.
- It may start without a page. It needs at least one way to start: a web
tool window (first page call), a
ui.commands[]entry (itsonCommand:<id>also starts the backend), theonWorkspaceOpenevent, or a provider the host queries: a toolchain or keymap import provider, or a language server withlaunch.kind: "backend", whose start asks the backend for its command line. A backend with none of them isplugin.manifest.invalid. - It is one process per plugin, shared by every open workspace and
window, on the machine that runs Lithe, also for remote workspaces. No
workspace is process state: each call names its workspace explicitly
(a context, below), so
${workspaceStateDir}is refused. - Starting on
onWorkspaceOpennever delays opening the workspace. The host starts such backends after the window is shown, one plugin at a time, and a backend that does not finishinitializewithin its initialize timeout is stopped and reported like any failed server start. A failed or disabled backend never blocks the workspace. - The
workspace.metadatapermission grants theworkspacehost service. It reveals the open workspaces' names, root folders and IDs, their authority, and the plugin's per-workspace state directory; it reads no file content. - Activation is gated only by the plugin being installed, enabled, and reviewed. Lithe has no workspace trust state at the moment: an operation that runs or changes something asks the user per operation, as the host's own controls do.
projectGenerators (requires projectGenerators: 0 in requires and the
project.generate permission) adds entries to the host's New Project window. The host owns the
window: the entry list and its search, the project name and location, the
"create Git repository" option, the empty project, and the Create button. A
generator declares only its own fields, the languages and build systems the
result needs, and the steps that produce it. No host code branches on a
generator ID.
"projectGenerators": [{
"id": "spring-boot",
"displayName": "Spring Boot",
"section": "generator",
"icon": {"path": "icons/spring-boot.svg", "mode": "original"},
"fields": [
{"id": "groupId", "type": "text", "title": "Group", "default": "com.example", "pattern": "[a-z][a-z0-9.]*"},
{"id": "packageName", "type": "text", "title": "Package name",
"default": "${field.groupId}.${project.name|package}", "pattern": "[a-z][a-z0-9.]*"},
{"id": "jdk", "type": "tool", "title": "JDK", "tool": "java", "minimumVersion": "17"}
],
"requirements": [{"languageID": "java"}, {"buildSystemID": "maven"}],
"steps": [{"kind": "archive", "url": "https://start.spring.io/starter.zip",
"query": [{"name": "packageName", "value": "${field.packageName}"}]}]
}]| Field | Meaning |
|---|---|
id |
Lowercase letters, digits, and hyphens; unique in the plugin. |
section |
project lists the entry under "New Project" (a plain project of a language); generator lists it under "Generators" (a framework or scaffolding tool). |
order |
0–1000, default 100. Entries are sorted by section, order, then name. |
fields |
The generator's inputs, drawn and evaluated in order. A field may read only fields declared before it. |
requirements |
languageID or buildSystemID (optionally when) the created project needs. The catalog lists the installed plugins that provide each, with their enabled state, so the host can offer to enable one or open the marketplace. A requirement never blocks creation. |
steps |
Run in order in a staging directory that becomes the project root. |
openFiles |
Project-relative files the host opens after creating the project, optionally with when. |
type |
Value | Properties |
|---|---|---|
text |
A string | default (a template over ${project.name} and earlier fields, re-evaluated until the user edits the field), pattern (matched as a whole), maxLength (default 256, at most 1024), placeholder, optional, maximumVersion |
enum |
One option | options (1–16 {value, title}), default (the first option otherwise), presentation (segmented or menu), maximumVersion |
boolean |
true or false |
default |
multiSelect |
A list of options, kept in declaration order | options (1–128, each with an optional group heading), default |
tool |
An installation of a tool definition, by home directory | tool (a tool definition ID), minimumVersion, pinAs, optional. See Tool fields. |
A field type is text, enum, boolean, multiSelect, or tool; any other
type is plugin.manifest.invalid. minimumVersion is allowed only on a tool
field, and optional only on a text or tool field.
maximumVersion is a template over earlier fields naming the highest version
the value may be, compared numerically by dotted components. It reads
${field.<id>.major} of a tool field: a target language level that must not
exceed the selected compiler declares "maximumVersion": "${field.compiler.major}".
While the bound expands to an empty string (no tool chosen yet) nothing is
checked.
Every field takes id (lowerCamelCase), title, description (shown behind
an info tip), advanced (listed under the collapsed "Advanced Settings"
group), and visibleWhen. A hidden field is not validated, and templates and
conditions read it as absent.
Text values never start with - and never contain control characters, so a
value cannot become an option or split a line. A project name is one
directory name: not empty, at most 128 characters, no /, \, :, or
control characters, not . or .., no leading -, and no surrounding
spaces.
A tool field lets the user pick one
installation of any tool that a plugin declares in
toolDefinitions, such as a Python interpreter, a Node.js
runtime, or a JDK. The generator names no host feature and no language: the
host lists installations through its generic tool discovery and the user's
tool settings, exactly as the tool's own settings page does.
{"id": "sdk", "type": "tool", "title": "Python", "tool": "python",
"minimumVersion": "3.11", "pinAs": "project"}| Property | Meaning |
|---|---|
tool |
Required. A tool definition ID: a short ID such as compiler defined by exactly one enabled plugin, or <pluginID>:<id>. There is no closed list; a tool nobody declares has no installations, and neither has a short ID several enabled plugins define, because it belongs to none of them. |
minimumVersion |
Optional, one to three dotted numbers. An installation below it is not selectable. |
pinAs |
Optional. project saves the selection as the created project's own tool choice (the same per-directory pin the tool's settings page edits), so the project builds and runs with the installation picked here. Without it the selection only feeds the steps. |
optional |
Optional. Without a usable installation an optional field stays empty and leaves no pin; otherwise the form reports required. An installation below minimumVersion is not usable. |
A tool field has no default; the host chooses the initial value. Core picks,
among the installations the host listed, the user's choice if it is still
listed and otherwise the newest version (the host's first among equal
versions). Versions compare as releases, so trailing zeros do not count:
3.11, 3.11.0, and 3.11.0.0 are equal. An installation below
minimumVersion is marked in the field's installations, so the host can
show it disabled. When the selection is below minimumVersion the form
reports toolTooOld, except that an optional field the user has not set
stays empty rather than picking an installation that is too old.
when and visibleWhen take one condition or a list that must all hold:
{"field": "<id>", "equals" | "notEquals": <value>} or {"field": "<id>", "oneOf": [<value>, …]}. Values are option values for enum, true/false
for boolean, and strings for text and tool (notEquals: ""
tests that an optional field is filled; a tool field compares as its home
directory). A condition on a hidden field is false.
Strings in the manifest use ${…}: ${project.name}, ${field.<id>} (a
text or option value, true/false, a comma-separated list, or the home
directory of a tool), ${field.<id>.version} and ${field.<id>.major} (tool
fields only: the version of the chosen installation as its definition's
version.pattern captured it, and that version's first numeric component,
21 for 21.0.4; a definition whose tool uses another numbering, such as a
legacy 1. prefix, normalizes it in the pattern), and ${field.<id>.executable}
(tool fields only: the path of the tool's executable in the chosen
installation). The three suffixed variables are rejected when the manifest is
validated if the field is not a tool field. Modifiers follow | and apply
in order: path turns dots into /, package makes a valid package name
(com.Example.my-app → com.example.myapp), and lower lowercases.
Template files use {{ … }} with the same variables, because POMs, Gradle
scripts, and properties files already contain ${…} that must reach the
project unchanged. Only {{ followed by project. or field. is a
variable; any other {{ is copied as text. A file name may also contain
${…} variables, although a step's target is the portable place for them.
A path segment that starts with dot- becomes a dotfile (dot-gitignore →
.gitignore), because packages keep no hidden files. Hidden files in a
template directory are skipped; symbolic links are refused.
kind |
Properties | The host… |
|---|---|---|
files |
source (package-relative directory), target (project-relative directory template), when |
writes the files of the plan, setting the executable bit where the template file has one. |
command |
command (a bare executable name), arguments (strings or {value, when}), environment, collect, mayUseNetwork, timeoutSeconds (default 600, at most 1800), when |
resolves the executable like a build tool, runs it with the staging directory as the working directory, and, with collect, moves the contents of that subdirectory up into the project root. Arguments that expand to an empty string are left out. |
archive |
url (no query or fragment), query ({name, value, when, omitEmpty}), when |
downloads the zip and calls project.extractArchive into the staging directory. |
Archive addresses must expand to HTTPS URLs (HTTP only for localhost,
127.0.0.1, or [::1]) without credentials. The URL may come from a field,
such as a configurable Spring Initializr server; the host shows the server
before creating. Template directories are limited to 1024 files, 1 MiB per
file, and 8 MiB in total; archives to 20,000 entries and 512 MiB extracted.
| Command | Payload | Result |
|---|---|---|
project.generatorCatalog |
{plugins: [{manifest, enabled}], hostCapabilities?} |
catalog: {generators: [{pluginID, pluginName, pluginEnabled, generator, requirements: [{languageID or buildSystemID, providers: [{pluginID, pluginName, enabled}]}], tools: [{fieldID, tool, providers: [{pluginID, pluginName, enabled}]}], usesNetwork}], issues: [{pluginID, error}]} |
project.generatorForm |
{manifest, generatorID, form} |
form: {projectName: {problem}, fields: [{id, visible, value, problem?, installations?}], activeRequirements, valid} |
project.generationPlan |
{manifest, generatorID, packagePath, form} |
plan: {projectName, steps, openFiles, toolPins: [{field, tool, pinAs, home, executable}], usesNetwork} |
project.checkName |
{projectName} |
name: {problem} |
project.extractArchive |
{archivePath, destination} |
extracted: {files} |
form is {projectName, values, edited, tools}. values maps field
IDs to values; a text value counts only when its ID is in edited, so derived
defaults keep following the fields they read. tools maps tool field IDs
to the installations the host found for the field's tool, in the order the
host prefers them, as [{home, executable, version}]; the field's values
entry is the home of the user's choice. A tool field in the result carries
installations: [{home, executable, version, meetsMinimum}], so the host
draws its picker from Core's answer, and its value is the installation Core
selected. A field problem is one of required, tooLong, invalidFormat,
unsupportedValue, toolTooOld, and versionTooHigh; hosts
localize by code. Plan steps are {kind: "writeFiles", files: [{path, contents, executable}]}, {kind: "command", command, arguments, environment, collect, mayUseNetwork, timeoutSeconds}, and
{kind: "archive", url, host, format: "zip"}. toolPins lists, for each
visible tool field with pinAs and an installation, the tool and the chosen
home and executable that the host saves as the project's pin.
The catalog entry's tools lists, for each tool field, the plugins that
declare the field's tool and whether they are enabled, in the same way as
requirements: the host can offer to enable one, or open the marketplace,
when the field has no installation. Core only names declarations; whether a
definition is trusted, and whether the tool is installed, is decided by the
host's tool registry and discovery.
After the steps, the host moves the staging directory to its final location
(refusing an existing one), saves the toolPins as the project's tool
choices through the same store the tool settings page edits, creates the Git
repository when asked, and opens the project. A failure or cancellation
removes the staging directory and leaves nothing behind
(plugin.project.generationFailed).
A plugin backend (a language server, build provider, or
plugin backend process the plugin starts) can offer
plugin functions: actions with
serializable params and results. A function is private to its plugin unless
the manifest exports it: the host invokes a private function on behalf of the
same plugin, for example from that plugin's own window, and another plugin
can call only an exported function.
Functions complement the
typed domain protocols; they never replace them. The protocol is implemented in
Rust Core (rust/crates/plugin-rpc/src/ and
rust/crates/plugin-function-relay/src/lib.rs).
A host starts every build provider process as a session of the plugin that
owns it and invokes its functions from that plugin's web pages (the
functions.* bridge methods below). A provider served by a shared language
server is started by the host for every plugin of its language, so it serves
no plugin's functions. The
official SDKs implement the backend side.
Every capability has one owner, who defines its contract, and one direction:
| Direction | Contract owner | Implemented by | Travels as | Examples |
|---|---|---|---|---|
| Plugin provides a domain capability | Lithe: the typed domain contract | The plugin, declaratively or by its backend | A manifest source, Core plans and decodes, a typed lithe/build/* request |
Build model, dependency tree; later task execution |
| Plugin provides a plugin function | The plugin: its own params and results | The plugin's backend | lithe/function/invoke |
Refresh a private snapshot, a step behind a button of the plugin's own UI |
| Plugin calls a host capability | The host | The host | lithe/host/* requests, offered by the protocol |
Run a process through the host's runner, write output, store state |
| Plugin calls another plugin's exported function | The exporting plugin: its own params and results, under an exported version | The exporting plugin's backend | lithe/host/functions/invoke, relayed by Core as lithe/function/invoke |
A language plugin asks a build plugin for the options it contributes |
Two rules keep these directions from forming a loop:
- A backend never asks the host to dispatch a domain operation or a function.
The host could route that request to the same backend, whose handler would
then wait for itself. A backend request for
lithe/function/invokeor anylithe/build/*method is refused withplugin.function.recursiveCall. A handler that needs, say, a Maven run asks for the host's process capability, never for the task that its own plugin implements. The relay methods are a different request: they name another plugin's exported function, Core refuses a call that would come back to a plugin already waiting on the call chain, and the old refusals above stay in force. - Host capabilities are primitive host services, never another entry into a
domain registry. Each is defined together with its params, permission, and
the operation it must belong to. A request for a service the host does not
offer is refused with
plugin.host.unavailable; the services are listed under Host services.
- A domain operation has exactly one implementation, chosen in the manifest
(
source:static,command, orprovider). Core plans only that one. There is no runtime fallback from a backend to a template or back: the two may differ in meaning, and a fallback would hide the failure. - A backend implements a domain operation through its typed method and Core's plan and decode commands, never through a plugin function, so IDE workflows, presentation, and other hosts keep a single contract.
- A plugin-private function needs no manifest field. What a running process
can answer is negotiated, as for build providers; so
declaring, adding, or removing private functions changes neither
pluginAPIVersionnor anyrequiresversion. Only a function that another plugin may call, and a call to such a function, are declared, inpluginFunctions. - A new kind of domain implementation is a new version of the capability that owns it, so only plugins that use it are filtered from hosts that cannot run it.
For a session owned by a plugin (lsp.startServer.pluginId), the host offers
capabilities.experimental.litheFunctions: { "versions": [0] } in
initialize, next to and independent of litheBuild. The protocol has no
version of its own: it is the version of backend. Host-owned sessions do
not offer or accept this protocol, retain no plugin-function registry, and
reject register/unregister with -32601. A plugin backend that offers
functions answers:
"experimental": { "litheFunctions": { "version": 0, "functions": [{ "id": "snapshot.refresh" }] } }functions is optional and defaults to none. An answer with a version the
host did not offer, or a malformed or duplicate function ID, does not
negotiate the protocol at all, so a session never starts with half a
registry. Unknown fields of the answer and of descriptors are ignored.
After a plugin-owned session successfully negotiates the protocol in
initialize, Core publishes its initial
registry as functionsChanged { functions: [<sorted IDs>], removed: [] },
including when the negotiated registry is empty. Hosts can discover the
functions from events without issuing an invocation first.
A function reference is { "pluginID", "functionID" }. The host routes by
plugin first: only a session the plugin started (lsp.startServer with
pluginId) serves its functions, and a reference to another plugin is
plugin.function.wrongPlugin. Host-owned servers serve none.
A function ID is dot-separated lowerCamelCase segments
(snapshot.refresh), at most 128 bytes, unique within the session. A
session holds at most 256 functions.
After initialize a backend may send:
| Request | Params | Effect |
|---|---|---|
lithe/function/register |
{ "functions": [{ "id" }] }, nonempty |
Adds the functions. |
lithe/function/unregister |
{ "ids": ["…"] }, nonempty |
Removes them and revokes their running calls. |
Each request is all or nothing: a malformed ID, a function listed twice, an
already registered ID, an unregistered ID, or exceeding the limit refuses the
whole request with JSON-RPC error -32602 and
data.code: "plugin.function.invalidRegistration". A successful change
answers null and publishes functionsChanged with the sorted IDs. A
backend that did not negotiate the protocol gets -32601 for both.
lsp.functionRequest { sessionId, operationId?, function, params?, timeoutMilliseconds?, context? } (context is described under
workspace contexts) returns { operationId } and sends:
{ "method": "lithe/function/invoke", "params": { "functionID": "snapshot.refresh", "operationID": "…", "params": { … } } }params must be a JSON object and defaults to {}. Core assigns every
backend invocation a unique, opaque operationID. The backend must echo it
unchanged in progress notifications; it must not derive it from the host's
operationId. The host continues to cancel and receive events using its own
operationId, which can be reused after a call ends. Core translates progress
back to that host ID, including requestProgress.result.operationID. This
prevents a late report for an earlier call from being attributed to a new call.
Core checks, in this order, and refuses without writing anything:
| Check | Core error | details |
|---|---|---|
| Plugin or function ID malformed | invalid_request |
plugin.function.invalidReference |
params not an object |
invalid_request |
plugin.function.invalidParams |
| Session belongs to no plugin | not_supported |
plugin.function.unavailable |
| Session belongs to another plugin | permission_denied |
plugin.function.wrongPlugin |
| Protocol not negotiated | not_supported |
plugin.function.unavailable |
| Function not registered | not_supported |
plugin.function.unknown |
The call ends with exactly one requestCompleted event:
| Outcome | Event |
|---|---|
| Result | result: { "value": <result> }; null is a result. |
| Backend error | error.code: "plugin.function.failed", the backend's message (at most 1024 bytes), and error.pluginErrorCode from error.data.code when it is an identifier of at most 64 bytes. The plugin code is for the plugin's own UI; Lithe never branches on it. |
No result, or a result over 1 MiB serialized |
plugin.function.invalidResult |
| Function withdrawn while running | plugin.function.revoked; the host also sends $/cancelRequest |
Cancelled through lsp.cancelOperation |
requestCancelled; the host sends $/cancelRequest |
| Deadline passed | requestTimeout; the host sends $/cancelRequest. timeoutMilliseconds is capped at one hour and defaults to the session's request timeout. |
| Process exited or the transport failed | The session's terminal error, as for every pending request |
After a call has ended, its late reply is ignored, and so is any progress
report for it. Every terminal path removes its function/request association;
withdrawing that function later cannot cancel a new operation reusing the
host ID, including a build request. A backend should stop work on
$/cancelRequest, but the host does not rely on it.
A backend reports progress of a running call with the notification
lithe/function/progress { operationID, message?, percentage? }, where
message is cut to 1024 bytes and percentage is a whole number from 0 to
100. Core publishes it as requestProgress with the operation ID and the
normalized report. A malformed report, or one for an operation that is not
running on that session, is dropped.
A backend may send these requests to the host.
Each service is granted by one manifest permission; Core receives the
granted services as lsp.startServer.hostServices (from the launch plan's
backend.hostServices) and checks every request before the host sees it.
| Request | Params | Result | Permission |
|---|---|---|---|
lithe/host/credentials/get |
{ key } |
{ secret }, null when nothing is stored |
credentials.store |
lithe/host/credentials/set |
{ key, secret } |
null |
credentials.store |
lithe/host/credentials/delete |
{ key } |
null, also when nothing was stored |
credentials.store |
- A
keyis 1–128 ASCII letters, digits,.,_,:, or-; asecretis a string of at most 16 KiB. - Credentials are private to the plugin ID: the host stores them in the
platform's secret store (the Keychain on macOS) in a namespace of that ID,
never in
${stateDir}or the package. They survive disabling and updating the plugin and are deleted when it is uninstalled. - Core refuses, without involving the host, in this order: a method the
protocol does not define (
-32601,plugin.host.unavailable), a service the plugin is not granted (-32010,plugin.host.permissionDenied), and malformed params (-32602,plugin.host.invalidParams; a field the method does not define is-32018,plugin.host.unsupportedOption, see Extending the host services). The code is inerror.data.code. - An accepted request becomes the runtime event
hostRequestwithmethodandresult: { hostRequestID, service, params }. The host answers it withlsp.hostResponse { sessionId, hostRequestID, result? | error: { message } }; Core sendsresult(ornull) to the backend, or-32603withplugin.host.failedand the host's message cut to 1024 bytes. An unknownhostRequestID, including one already answered, isinvalid_request. - A session holds at most 16 unanswered host requests; a further one is
refused with
plugin.host.failed. Unanswered requests end with the session. hostServicesneedspluginIdand names only services this Core defines (credentials,workspace, or registered method-grant names); otherwiselsp.startServerisinvalid_request.
The requests below give a backend capability discovery, subscription disposal, and workspace contexts. A context is named on each call, host failures carry typed details, and the host can cancel requests and send host notifications.
| Request | Params | Result | Granted by |
|---|---|---|---|
lithe/host/getCapabilities |
none or {} |
{ protocolVersion, backendAuthority, services, limits } |
every plugin-owned session |
lithe/host/subscriptions/dispose |
{ subscriptionID } |
null, also for an unknown or already disposed ID |
every plugin-owned session |
lithe/host/workspace/listContexts |
none or {} |
{ revision, contexts: [<context>] } |
workspace.metadata |
lithe/host/workspace/getContext |
{ context: { contextID, generation } } |
<context> |
workspace.metadata |
lithe/host/workspace/getFolders |
{ context: { contextID, generation } } |
{ folders: [<folder>] } |
workspace.metadata |
-
Core answers
getCapabilitiesitself;protocolVersionis the negotiatedlitheFunctionsversion.servicesmaps each service the session may use (the implicithost,subscriptions, andcommands, and the granted ones) to{ methods, notifications }: the requests it may send and the host notifications it may receive. It lists nothing the session may not use, and a host grants only the services it has handlers for, so a listed method never fails for want of one.backendAuthorityis"local": the backend runs on the machine that runs Lithe.limitsgivespendingHostRequests,functionResultBytes,hostRequestBytes,hostResultBytes,errorMessageBytes(the cut of a host failure's message),errorDetailsBytes, andnotificationBytes. A backend must checkservicesbefore it calls a method. -
A service entry may also carry
options: what the host declared for that service. Wheneditor/registerActionsis granted,services.editor.optionshasactionIcons(the icon tokens the host draws) andlimitsgainseditorActionSourceBytes(the largest document the host sends a collector). Wheneditor/registerProvidersis granted,services.editor.options.providerTypeslists the provider types the host routes. -
A context is one open workspace in one window:
{ "contextID": "w-3f2a9c", "generation": 2, "name": "orders", "authority": { "kind": "local" }, "folders": [{ "rootID": "r0c1d2e3f4a5b6c7d", "name": "orders", "path": "/Users/me/orders" }], "stateDirectory": "/Users/me/Library/Application Support/Lithe/PluginState/<plugin>/backend/workspaces/<digest>" }contextIDuses the key alphabet and lives as long as the window shows that workspace: a window that switches to another workspace closes the context and opens one with a newcontextID, so state a backend keyed by the ID never carries over to a different workspace.generationstarts at 1 and grows every time the same workspace's folders or authority change; a request naming an older generation, or a closed context, isplugin.host.contextExpired, and the backend asks again.revisionoflistContextsgrows whenever a context opens, closes, or changes. -
rootIDidentifies one root folder stably for the same location, across restarts, without revealing the path. It is keyed per plugin with a host secret, so two plugins see different IDs for the same folder and an ID cannot be computed from a guessed path.pathis present only for alocalauthority. -
authority.kindislocalorremote(a remote workspace, which also hasauthority.id, an opaque connection ID). For aremotecontext the folders carry nopath: the local directory the host keeps for a remote workspace is an empty placeholder, not the workspace. File, search, and process services answer a request for a context whose authority they do not implement withplugin.host.authorityUnsupported; they never read the placeholder or run the operation locally instead. -
stateDirectoryis private to the plugin and the workspace, outside the plugin package and the application bundle. OnlygetContextreturns it, and creates it then;listContextsentries carry every other field and create nothing, so listing many windows leaves no directories behind.${stateDir}is the plugin-wide state. Both survive disabling and updating the plugin and are deleted when it is uninstalled. -
A page call may name its context:
lsp.functionRequest.context({ contextID, generation }) reaches the backend asparams.contextoflithe/function/invoke. A malformed reference isplugin.function.invalidParams. The host fills it from the page's window. -
A host failure may carry a code and details:
lsp.hostResponse.error: { message, code?, details? }. Core passes these codes to the backend, each with its own JSON-RPC error code so a backend can branch on the number alone; any other code, or no code, givesplugin.host.failed.detailsis an object of at most 4 KiB serialized (for example{ currentVersion }withversionConflict, or{ retryAfterMilliseconds }withrateLimited); it reaches the backend aserror.data.details. Details that are not an object, or larger, are dropped; the failure itself still reaches the backend.fixtures/backend/function-protocol.json(hostFailures) lists the mapping.Code JSON-RPC Meaning plugin.host.unavailable-32601The host offers no such service or method plugin.host.invalidParams-32602Params malformed or out of range plugin.host.failed-32603Any other failure plugin.host.cancelled-32800The request was cancelled plugin.host.permissionDenied-32010Permission not declared, or the user withheld consent plugin.host.contextExpired-32011The context is closed or its generation replaced plugin.host.authorityUnsupported-32012The service does not support the workspace authority plugin.host.versionConflict-32013A document or resource version differs from the expected one plugin.host.limitExceeded-32014A host limit was exceeded plugin.host.timeout-32015The host's deadline passed plugin.host.notFound-32016The named resource does not exist plugin.host.readOnly-32017The resource or workspace cannot be written plugin.host.unsupportedOption-32018The host does not implement a requested option; details.optionnames the fieldplugin.host.preconditionFailed-32019A state the request requires does not hold, such as a missing user confirmation plugin.host.busy-32020The resource is in use; retry later plugin.host.rateLimited-32021Too many requests in a period; retry after details.retryAfterMillisecondswhen givenplugin.host.languageServerUnavailable-32022The plugin's language server is not running for the context, or stopped while the request was in flight plugin.host.languageServerError-32023The language server answered with a JSON-RPC error; detailsis{code, message}plugin.host.methodNotDeclared-32024The method, or the workspace/executeCommandcommand, is not in the server's declared sessionplugin.host.toolUnresolved-32025A tool definition the request names resolves to no executable for this workspace; detailsis{ tool, reason },reasonbeingnotFound(no candidate of the four layers meets the definition and its minimum version) orunusable(a chosen executable cannot be used)plugin.host.foreignContribution-32026The named server or contribution belongs to another plugin JSON-RPC reserves
-32768to-32000for the protocol and lets implementations define-32099to-32000as server errors.-32000to-32009stay free for backends' own failures, which many JSON-RPC libraries already use from-32000, so host codes start at-32010and grow by one; a code is never reused for another meaning. -
A backend may cancel its own pending host request with
$/cancelRequest. Core answers it at once with-32800andplugin.host.cancelled, frees the slot, and publisheshostRequestCancelled { hostRequestID }so the host stops the work; a laterlsp.hostResponsefor that ID isinvalid_request. The 17th pending request of a session isplugin.host.limitExceeded. -
A service that creates a subscription returns a
subscriptionIDof the key alphabet; its notifications name it. Ending the session, disabling the plugin, or closing the subscription's context disposes it. -
A backend still cannot ask the host to call a function or a domain operation (
plugin.function.recursiveCall), so a page call never waits on itself through the host.
The host tells a backend about changes with JSON-RPC
notifications; the backend answers none of them. The host sends them through
Core with lsp.hostNotify { sessionId, method, params }, which returns
{ delivered }, true for an accepted notification.
| Notification | Params | Granted by |
|---|---|---|
lithe/host/workspace/didChangeContexts |
{ revision } |
workspace.metadata |
lithe/host/subscriptions/notify |
{ subscriptionID, sequence, event } |
every plugin-owned session |
lithe/host/commands/didRun |
{ commandID, context? } |
every plugin-owned session |
lithe/host/run/didExit |
{ runID, exitCode?, signal? } |
run.start |
lithe/host/languageServer/didChangeState |
{ context, serverID, state, generation } |
languageServer.request |
lithe/host/languageServer/didReceiveNotification |
{ context, serverID, generation, method, params?, truncated? } |
languageServer.request |
didChangeContextsfollows every changerevisionoflistContextscounts; the backend callslistContextsfor the contexts. A backend that starts later reads them itself.subscriptions/notifycarries oneeventobject of the named subscription.sequencestarts at 1 and grows by one per subscription, so a backend can detect a gap; nothing follows a disposal.commands/didRunreaches the plugin's backend when the user runs one of the plugin's commands.commandIDis the command's ID;context({ contextID, generation }) is the window's context when it has one. A running backend receives it in order with the other notifications; a stopped one is started for it only when it declaresonCommand:<commandID>, and receives it once ready.run/didExitreports the end of a Run session the backend started withrun/start.exitCodeis the process's exit status andsignal(1 to 127) the signal that ended it; both are absent when the host lost the process. It is sent once per run, only to a session that holds therun.startgrant.revisionandsequenceare positive integers,eventis an object, and serialized params are at mostlimits.notificationBytes. Core refuses any other method, a service the session is not granted, malformed params, a session that is not plugin-owned, or one that is not ready, withinvalid_request.fixtures/backend/function-protocol.json(hostNotifications) holds the cases.
Host capabilities are added without a new version, and a backend discovers
them with getCapabilities instead of a version number. A later host adds,
without a new version:
- a service with its methods and notifications, granted by a new manifest permission, or a method or notification of an existing service;
- an optional field of request params (an option) whose absence keeps the
earlier behavior, or a field of a result, a notification,
details, orlimits; - a failure code, which takes the next unused JSON-RPC number after
-32021; a number never changes meaning.
A change an existing backend would misread gives backend version 1, and the
protocol moves with it: removing or renaming a method, notification, or
field, changing a field's meaning or type, making an option required,
changing a code's number, or weakening an ordering or lifecycle guarantee
above.
So that a backend works with every host:
- It calls a method only when
getCapabilities.serviceslists it; another is-32601,plugin.host.unavailable. - It ignores result, notification,
details, andlimitsfields it does not know, and anylithe/host/*notification it does not know. - Core refuses a field the method does not define with
-32018,plugin.host.unsupportedOption, anddetails: { option }naming it, before the host sees the request; the backend may retry without that option. A service whose params Core does not type answers the same way itself. - It treats a JSON-RPC number it does not know as a host failure, and
branches on
error.data.codewhen it knows that code. - A package whose backend needs a new service declares the new permission.
A host that does not know the permission refuses the package at install
(
plugin.permission.unknown), so a permission is never granted or ignored silently.
fixtures/backend/function-protocol.json (hostRequests) holds the
unsupported-option cases.
A host that advertises backend support implements these methods.
getCapabilities lists only the intersection of the plugin's
permission-derived grants and this host's registered method handlers.
Grants in lsp.startServer.hostServices for the methods below use full
method names, not service names. The workspace grant admits only
listContexts, getContext and getFolders; it never grants file reads. The
credentials grant admits the three credentials methods. Registering another
handler cannot replace a method already registered.
All methods below require context: {contextID, generation}. Resource arguments
use {rootID, path} from that plugin's getFolders response; paths use / and
are relative to the root. A root ID is not a lifetime or an authorization token.
Opening the same folder in two windows creates independent context-owned leases;
closing one does not invalidate the other, and reopening works with generation 1
in its new context. Remote contexts are explicitly refused by these
methods. Unknown options fail before side effects with unsupportedOption.
Methods (prefix lithe/host/) |
Permission | Params in addition to context |
Result |
|---|---|---|---|
documents/list |
documents.read |
none | {documents:[<summary>]} |
documents/getSnapshot |
documents.read |
documentID |
{document:<summary>, text} from the dirty live buffer |
editor/registerActions |
editor.actions (+ documents.read) |
id,function,selector |
{subscriptionID}; independent editor actions, see below |
editor/registerProviders |
editor.providers (+ documents.read) |
id,selector,providers |
{subscriptionID}; editor language-feature providers, see below |
editor/getActive |
documents.read |
none | summary of the foreground text tab, or null |
editor/getSelections |
documents.read |
documentID |
{documentID,version,selections:[{anchor,active}],primaryIndex} |
editor/applyEdits |
editor.edit |
documentID,expectedVersion,edits:[{range,text}] |
{documentID,version,applied} |
editor/replaceSelections |
editor.edit |
documentID,expectedVersion,selections:[{anchor,active}],replacements:[string] |
same; one replacement for all selections or one per selection |
editor/insertText |
editor.edit |
documentID,expectedVersion,at,text |
same |
workspace/readDirectory |
workspace.files.read |
resource,cursor?,limit? |
{entries:[{name,resource,kind}],nextCursor} |
workspace/stat |
workspace.files.read |
resource |
{resource,kind,size,modifiedMilliseconds} |
workspace/readFile |
workspace.files.read |
resource,maximumBytes? |
{resource,source:"disk",byteCount,isTruncated,identity,text?,encoding?,base64?} |
workspace/watch |
workspace.files.watch |
resource,recursive? (default false) |
{subscriptionID}; events contain {rootID,changedPaths,requiresRescan} |
workspace/findFiles |
workspace.search |
resource naming the root (path:""), query,fileMask?,cursor?,limit? |
{matches:[<resource>],nextCursor,isTruncated,source:"disk"} |
workspace/findText |
workspace.search |
root resource,pattern,fileMask?,caseSensitive?,wholeWords?,regularExpression?,cursor?,limit? |
same envelope, matches {resource,line,preview,range:null}; the upstream search owner currently reports lines, not exact match spans |
editor/open, editor/reveal |
editor.open |
resource,activation? (select or background), range?,tab? (regular only) |
document identity/version and state, wasAlreadyOpen,windowIsForeground,appliedSelection |
preview/openHTML |
preview.open |
HTML resource,presentation? (editorAndPreview or previewOnly), range? |
open result plus usesLiveBuffer:true,presentation; explicitly changes the existing tab's presentation |
browser/openExternal |
browser.open |
exactly one of HTTP(S) url or HTML resource; unsaved? (openSavedFile or requireSaved) for a resource |
`{opened:true,source:"web" |
window/showInformation, window/showWarning, window/showError |
window.messages |
text,actions? |
{selectedIndex}; zero-based, or null on dismissal |
A document summary has documentID,version,path,languageID,isReadOnly,isDirty, encoding,lineEnding. path is workspace-relative or null for a document outside
the root; languageID may be null. Positions {line,column} are one-based UTF-16;
ranges {start,end} are half-open and cannot split a surrogate pair. Selections
keep anchor/active direction. Batches are atomic and one Monaco undo step. Stale
versions, read-only buffers, pending editor synchronization and IME composition
are refused. Background documents without a mounted editor can be listed/read,
but edits/selections may return busy; opening a foreground tab does not imply
that its web editor is already ready. Retry reads after readiness, not edits
blindly. editor.open/reveal currently applies a range's start as a caret and
reports {kind:"caret",line,column}, not a fabricated full selection.
Limits: snapshots and file reads expose at most 128 KiB of source text/bytes per
call; oversized snapshots return limitExceeded, file reads report truncation.
This leaves room for worst-case JSON escaping under the 1 MiB serialized host
request/result budget. Batches contain at most 1,000 edits/selections. Directory
pages default to 200 and cap at 1,000; search pages default to 100 and cap at 200,
with the search owner's overall 200-result cap. Explicit file reads use
component-wise O_NOFOLLOW; listings omit symlinks. Hidden-file rules affect
listing/search/watch, not authorization for explicit reads. Open/browser paths
retain the existing canonical containment checks and their documented
check-to-open race; native backends are not OS-sandboxed.
Machine host requests have a 30-second host deadline. User-choice messages wait
until the user answers, the request is cancelled, or the context/session closes.
Messages retain the per-plugin/window rate and queue limits (5/10 seconds,
3 pending per plugin, 10 per window, 2,000 text characters, 3 actions of 40
characters). Session exit revokes grants and cancels requests/subscriptions;
old session callbacks cannot affect a replacement. Closing a context cancels its
requests with contextExpired. Notifications have a 128-entry per-session queue;
context revisions coalesce to the latest, and overflow ends the session instead
of allowing unbounded memory or silently losing commands. Notification/response
writes share the process write watchdog and stop cleanup with function requests.
Cancellation or timeout does not promise rollback: an edit or external open
may already have committed. The SDK must not replay a mutation on these failures.
On versionConflict, reread and recompute; on contextExpired, resolve the
original target again, never substitute the focused window. busy/rateLimited
may be retried with bounds; permissionDenied/readOnly need user intervention.
terminal-backend.plugin.json is the
minimal manifest. terminal.create grants lithe/host/terminal/create, show, close, reopen,
dispose, and onExit. It requires backend; discover the individual
methods through getCapabilities before using them. A host without this
handler does not advertise its methods. The
plugin's web page calls its backend through functions.invoke; the backend
uses these host requests with the invocation's workspace context. There is
no direct web terminal.* bridge.
Method (prefix lithe/host/terminal/) |
Params, in addition to context |
Result |
|---|---|---|
create |
executable, optional arguments, title, cwd, environment, show, retainedContextBytes |
{terminalID,generation:1} |
show |
terminalID |
null |
close |
terminalID, optional retainedContextBytes |
{retainedContextBytes} after bounded native cleanup |
reopen |
terminalID, optional show |
{terminalID,generation,restoredContextBytes} |
dispose |
terminalID |
null after closing and forgetting the handle |
onExit |
terminalID |
{subscriptionID} |
executable is an absolute path or a bare command resolved by the native
terminal's PATH lookup; relative paths containing separators are refused.
arguments is an argv array, never shell text. cwd is a {rootID,path}
workspace directory resource (an empty path selects the folder root); without
it, the workspace root is used. The native adapter rejects a canonical cwd
outside that workspace. environment maps variable names to string overrides
or null removals. Process-ownership scope variables cannot be overridden.
show defaults to true; it selects the terminal in its own window without
raising a background window. With show:false, creation and reopening preserve
the current terminal selection and do not reveal the terminal panel or request
input focus; the native tab is mounted when the plugin explicitly calls show.
Unknown options fail before any side effect.
Creation and reopening ask for execution consent with the program, argv and
working directory, all available in a scrollable, selectable native text view,
then revalidate context and cancellation before launching
through the existing PTY terminal module. The response waits for user choice
without a machine-operation deadline, but cancellation/session/context closure
still ends it. Native launch and process cleanup retain their own bounds.
The host never types a launch into an existing shell or reads other terminals.
There is no sendText, user-terminal enumeration, web emulator, or Docker
special case. First release is local authority only; a remote context fails
with plugin.host.authorityUnsupported instead of starting anything locally.
Each handle belongs to one plugin backend session and one context generation.
A handle from another plugin, context or replacement backend is notFound.
At most eight handles per plugin are retained, including closed handles;
dispose releases one. Limits are 256 arguments, 16 KiB per argument or
environment value, 64 environment entries, 64 KiB combined launch data, and
128 UTF-8 bytes in a nonempty single-line title. busy refuses concurrent
close/reopen/dispose on the same handle. Reopening a running terminal is
preconditionFailed; showing a closed terminal asks the plugin to reopen it.
close ends the process tree and removes the native tab, keeping the handle's
launch configuration and a plain-text suffix of its displayed terminal output.
retainedContextBytes is a nonnegative UTF-8 byte budget: default 16384,
maximum 262144, and 0 disables history. It can be chosen at create or
changed at close; over-limit values are refused, never silently expanded.
The result reports actual retained bytes, which can be smaller to avoid cutting
a Unicode scalar. The history is a rendering snapshot, not input events, raw
escape sequences, a resumable process image, or shell state. Secrets already
printed or echoed by a program can be part of that display; choose 0 when no
history should be retained.
reopen uses the saved executable, argv, cwd and environment in a new PTY
process. It feeds retained plain text to the display before spawning, never to
stdin. Shell variables, unsubmitted input, process state and automatic command
replay are not restored. Natural exit and user tab close are also reopenable;
the snapshot is bounded by the same budget. It keeps terminalID and increments
generation; late exit callbacks of previous generations are ignored. These
handles/history exist only in memory, not across app/backend restart or
workspace close. No state is written to the app bundle or plugin package.
Natural exit and user tab close run the same bounded native cleanup before
retaining the budgeted snapshot and releasing the complete native session and
render buffer. The handle is busy during cleanup; its exit notification is
sent after cleanup, so a subscriber can reopen immediately. A zero-byte budget
retains no display text on any close or exit path.
dispose closes any live process and permanently forgets the launch settings
and history. Ending the backend session, disabling/updating the plugin or
closing/replacing its workspace does the same for only its owned terminals;
user-created terminals are unaffected. Closing a page alone does not close a
completed terminal creation. Cancellation before launch creates nothing;
cancellation or context loss during launch closes the newly created process.
Cancelled creation/reopen is not safe to retry automatically.
onExit supports one live subscription per handle. Its event is
{type:"exit",terminalID,generation,exitCode,reason}, with an integer or null
exit code and reason:"exited"|"closed". Each generation publishes one outcome.
Subscribing after exit delivers its recorded outcome once; the subscription
continues across reopen. Disposing it stops notifications and keeps the
terminal open. A disposed terminal has no further events.
The executable and argv remain plugin-owned semantics. A container plugin can launch the user's Docker CLI with an explicit engine and container ID; another tool can launch its interactive client through exactly the same service. Neither requires a fake Build/Run provider.
A backend can start a session that the host presents as its own: a Run session
in the Run tool window, or a debug session on one of the plugin's own adapters.
The backend decides what to start; the host owns the process, the console, the
Stop button and the end of the session. A Run session adds host presentation
(a Run window entry the user can stop, the console, the exit status) on top of
what process.run already allows; it grants no other capability. run/start
and debug/start take context: {contextID, generation} and the permission
shown. run/stop takes no context, so a run can still be stopped after the
context it started in has closed.
Method (prefix lithe/host/) |
Permission | Params | Result |
|---|---|---|---|
run/start |
run.start |
context, label, process, temporaryFiles?, debug?, rerun? |
{runID} |
run/stop |
run.start |
{runID} or {sessionID} |
null |
debug/start |
debug.start |
context, label, adapter, request, configuration |
{sessionID} |
run/start:
labelis 1 to 256 characters without control characters. It titles the Run window entry.processis{executable, arguments, workingDirectory, environment}, all four required.executableis{kind: "tool", tool}, a tool that the manifest lists inrequiresTools(the host resolves it through the tool-definition layers), or{kind: "packaged", path}, a normalized relative path of a file inside the plugin package. Arguments, the working directory (relative to the workspace root and an existing directory inside it) and the environment have the bounds of run preparation. An unknownprocessfield is refused.temporaryFilesis at most four files of up to 4 MiB each. A file is{name, contents}, text the host writes as UTF-8, or{name, base64}, bytes in standard base64 the host writes unchanged; a file with both or neither is refused attemporaryFiles[<n>]. Bytes are for a format whose encoding the plugin must choose, such as a launcher that reads its argument file in the system code page: the plugin encodes the file and the host never transcodes it. The host writes the files into its own temporary directory before the process starts, removes them when the run ends, and replaces${temporaryFile:<name>}inprocess.argumentsandprocess.environmentwith the file's path. A name is unique, 1 to 64 letters, digits,.,_or-, and starts with a letter or digit. A plugin therefore never needs to know where the host keeps temporary files.debugis{configurationType, target}and makes the run a debug session.configurationTypeis a public debug adapter alias, as in registry connection debugging.targetis{kind: "connection", port}: the process listens for a debugger onport(1 to 65535; the backend puts it into the arguments), the host waits for it and attaches the adapter. Or{kind: "launch"}: the adapter starts the process itself, taking program, arguments, directory and environment fromprocessthrough itstargetBindings.rerunis{function, data}: one of the plugin's registered functions and its params, a JSON object of up to 64 KiB. When the user chooses Run again, the host invokes the function withdataas the params and the currentcontext, and the backend answers with a newrun/start, so the backend prepares the restart. Withoutrerunthe entry cannot be rerun.- The reply carries an opaque
runID. The host sendsrun/didExitwhen the process ends.
run/stop stops a run by runID or a debug session by sessionID and answers
null, also when it already ended. Cancelling the host request is the same as
stopping. A backend can stop only runs and sessions it started.
debug/start:
labelis as inrun/start.adapteris one of the plugin's own adapters, by its local ID or its qualified<plugin-id>/<local-id>ID. An adapter of another plugin is refused withplugin.host.notFound, and one the plugin does not declare isplugin.host.notFoundtoo.requestislaunchorattach, and the adapter must serve it.configurationis a JSON object of at most 256 KiB that the host passes to the adapter as the DAP launch or attach arguments, on top of the adapter's declared defaults.noDebugand every key that starts with__belong to the host and are refused.
Both methods refuse malformed params with plugin.host.invalidParams; the
rejection from Core (plugin.run.invalidStart or plugin.debug.invalidStart,
with the field) is in details.code and details.field. A closed context is
plugin.host.contextExpired, and a tool that resolves to no executable is
plugin.host.toolUnresolved.
The host keeps running processes when the plugin is disabled or its backend
exits; it only stops telling the backend. The run then ends the way a run
without a backend ends. A backend that starts again does not receive
run/didExit for a run it did not see begin.
Fixtures: fixtures/run/run-start.json (what run.decodeStart returns for each
case, including the rejections) and fixtures/debug-adapters/debug-start.json (the same for
debug.decodeBackendConfiguration). Temporary files depend on the host's
temporary directory and are covered by Core's own tests.
The languageServer host service lets a plugin backend talk to the language
servers its own plugin declares. It is granted by the languageServer.request
permission, only for a language server that lists requestMethods
or forwardNotifications, and each method is granted by its full name.
getCapabilities lists the methods this host implements. A host without
backend support advertises none of them.
Every method takes context: {contextID, generation} and serverID, the
languageID of one of the calling plugin's servers. A server of another
plugin is plugin.host.foreignContribution.
Methods (prefix lithe/host/languageServer/) |
Params in addition to context, serverID |
Result |
|---|---|---|
request |
method, params? (an object, default {}), timeoutMilliseconds? (1 to 300000, default 60000) |
{value}: the server's JSON-RPC result, which may be null. |
notify |
method, params? |
{delivered}. false only for workspace/didChangeConfiguration sent before the server is initialized; the value is then sent with initialized. |
setStatus |
state (preparing, ready, degraded, or failed), message?, percentage? (a whole number from 0 to 100) |
{} |
requestandnotifyare checked by the session policy:plugin.host.methodNotDeclaredfor a method or command the server did not declare,plugin.host.languageServerUnavailablewhen the server is not running for this context,plugin.host.languageServerErrorwhen the server answers with an error (details: {code, message}),plugin.host.timeout,plugin.host.cancelled,plugin.host.busyfor the 129th waiting request, andplugin.host.limitExceededfor a result over the host's result bound. Cancelling the host request ($/cancelRequest) cancels the server request.setStatusshows the plugin's own preparation state next to the server in the host's status UI:preparingwith an optionalmessageandpercentagewhile the plugin imports a project or indexes,readywhen the server can answer,degradedwhen it works with reduced results, andfailedwhen it cannot.messageis user-facing text of the plugin, shown as given and cut to 1024 bytes on a character boundary. The latest status replaces the earlier one of the same server; a new servergenerationstarts without one, and the host removes it when the server stops. It is notlithe/function/progress, which belongs to the function call that is running, while preparation outlives every call.- The language server keeps no state of its own in the host: when the process
stops or crashes, every request in flight ends with
plugin.host.languageServerUnavailable, and the backend asks again afterdidChangeStatereportsrunningwith a newgeneration.
Notifications to the backend, granted by the same permission:
| Notification | Params | Meaning |
|---|---|---|
lithe/host/languageServer/didChangeState |
{context, serverID, state, generation} |
state is starting, running, stopped, or crashed. |
lithe/host/languageServer/didReceiveNotification |
{context, serverID, generation, method, params?, truncated?} |
A notification the server sent and forwardNotifications lists. |
generation is a positive integer the host raises each time it starts the
server for this context, so a backend can tell which process a state or a
notification belongs to; the host never delivers one of a replaced process
after the new one has started. A forwarded notification larger than
limits.notificationBytes is delivered once with truncated: true and
without params. Core enforces the request policy (whitelist, gates,
serialization, timeouts) when it relays a request, and
fixtures/backend/function-protocol.json holds the request, response, and
notification shapes every host must reproduce.
A plugin whose server hands out documents that have no file (for example the decompiled source of a library class) serves them as a URI scheme:
{
"context": {"contextID": "w1", "generation": 1},
"scheme": "metals-library",
"function": "documents.read"
}lithe/host/documents/registerContentProvider, granted by
documents.contentProvider, returns {subscriptionID}; dispose it with the
existing subscription API. Registration only records the binding and never
calls the backend; the function must already be registered on the session.
- A scheme is 1 to 64 lowercase ASCII letters, digits,
+,-, or., and starts with a letter. The schemes the host or the platform resolves are refused:about,blob,data,file,ftp,http,https,javascript,lithe,mailto,untitled,ws, andwss. A scheme has one provider per workspace window; registering it again isplugin.host.preconditionFailed. - When a document whose URI has that scheme is opened, the host calls the
function through
lithe/function/invokewith the workspacecontextand{uri}. The answer is a string, or{content: string}, of at most 1 MiB. The document opens read-only; any other answer, a failure, or a timeout (the host waits 30 seconds) shows the user an error and opens nothing. - The provider is the only source of the text. The host keeps no copy once the document closes, and it asks again when the document is reopened. Ending the session or closing the context withdraws the provider.
A command in ui.commands may omit toolWindowID, and toolWindows may be
empty or omitted. A command without toolWindowID requires backend, the
ui.contribute and backend.launch permissions, and its onCommand:<id>
event. See fixtures/backend/command-backend.plugin.json.
The command appears through the ordinary workbench command contributions and
sends commands/didRun without opening a dummy tool window. Commands arriving
while initialize is pending wait until ready; a crashed backend can start again
on its declared command event. Background workspace activation has two concurrent
start slots; explicit command activation does not wait for unrelated background
plugins. didRun remains an event, not an acknowledgement that the command's
business action succeeded; a future result-bearing execute API must use the
existing request lifecycle rather than infer success from notification delivery.
Editor actions are a host presentation capability of an ordinary tool backend, not an LSP, Build, Run, Test or Debug provider. A play icon is presentation, not an execution kind. The host understands the bounded range/title/icon/invocation shape; HTTP/SQL parsing and execution remain private to the plugin. Native domain workflows still use their own typed contracts, never a private-function substitute.
A backend declares backend, documents.read and editor.actions, discovers
lithe/host/editor/registerActions with getCapabilities, then registers per
workspace context:
{
"context": {"contextID": "w1", "generation": 1},
"id": "requestActions",
"function": "requests.collectActions",
"selector": {"fileExtensions": ["http", "rest"], "fileNames": []}
}- Registration returns
{subscriptionID}. Dispose with the existing subscription API. It only records the binding and never calls the backend while answering; collection runs later. The function must already be registered on that session. - No manifest
runMarkerssection or capability, no special command notification, and no language-server/Run declaration is added. The first implementation starts through existingonWorkspaceOpen(or an existing explicit entry); registration cannot discover a backend that has not started. Backend installation still needs restart. Standalone backends have an application-scoped enablement module, without a fake UI or language provider; safe mode/quarantine/disable still veto their startup. Example package:fixtures/editor/editor-actions.plugin.json. - A selector has 1 to 32 literal names/extensions; extensions accept an optional leading dot. Matching is case-insensitive. At most 32 bindings per plugin across contexts; duplicate registration IDs in a context are refused.
- The host invokes the collection function via existing
lithe/function/invokewith workspace context and{document:{documentID,version,path},text}. Version is the same public buffer version asdocuments/getSnapshot, not a WebView revision. Source is the dirty live buffer, capped at the host'slimits.editorActionSourceBytes. Remote workspaces and read-only preview surfaces are not served. - Collection is read-only by contract: no network request or business execution. Edits invalidate the old actions immediately and cancel older collection work; the host debounces fresh collection and rejects stale or withdrawn results. The host bounds the whole collection batch to eight seconds, not eight seconds per binding. Refresh acknowledges scheduling without waiting for collectors; a slow or failed collector must not fail editing or the save synchronization barrier.
- Return
{actions:[{id,range:{start:{line,column},end:{line,column}},title,icon, invoke:{functionID,params?}}]}. Positions are one-based UTF-16, half-open and cannot split surrogate pairs. Each range stays in the exact input document.idis 1 to 128 bytes without controls, unique per answer;titleis 1 to 200 characters on one line.iconis a host-owned presentation token fromservices.editor.options.actionIcons. At most 1000 actions and 1 MiB serialized; malformed answers are refused whole. Same-line actions share a menu rather than silently replacing each other. - An action may carry
alternatives: at most 4{id,title,icon,invoke}entries with the same rules and IDs unique within the action, offered in the action's menu after the action itself (for example Debug or Test beside Run). - The Lithe desktop host draws
run,run-all,debug,test,test-passed,test-failed,test-skipped,navigate-superandnavigate-implementations. An icon names a picture, not an operation: a test icon does not make the action a test, and the host never starts anything because of an icon. - Each invocation target is a currently registered function of the same plugin;
paramsis a plugin-private JSON object. The host stores it behind an opaque per-batch action key, never accepts invocation payloads from the web editor, and validates document/context/session/registration again on click. It calls the function with{documentID,expectedVersion,actionID,data:<params>}through the ordinary invocation lifecycle; a chosen alternative addsalternativeIDand passes that alternative'sparamsasdata. The plugin validates/re-reads the live document before side effects. - A click result may carry
{reveal:{locations:[{uri,range}]}}: 1 to 100 absolute URIs with zero-based LSP ranges. The host opens a single location and lists several in its navigation results. Other result members stay the plugin's own. - A click that runs or debugs code starts it through the host's run and debug
methods (
lithe/host/run/start,lithe/host/debug/start) oncegetCapabilitieslists them, so the run appears in the host's Run and Debug windows. A private process started by the plugin is not a substitute. Click executions are not cancelled by unrelated re-parsing; withdrawing the binding, ending its session/context or tearing down the owning editor cancels them. The host never retries side effects automatically. - Toolbar and menu Run/Debug in the Lithe desktop reuse these actions for a language the
package declares in
languageswithout an execution or debug module and without the matchinghostCapabilitiesentry: with a current-file Run configuration the host saves the document, collects the document's actions as the gutter does, and invokes the first action (ordered by range start) whose icon isrunorrun-all; Debug invokes that action's first alternative whose icon isdebug, else the first action whose own icon isdebug. No matching action, a stale document or a withdrawn binding produces a user-visible notice and nothing starts. This is only a different entry point to the plugin's own function; the host still never starts code because of an icon, so the plugin decides what the action does and still useslithe/host/run/startorlithe/host/debug/startto run it. - Registration/collection validation is the shared Core command
plugins.editorActions.decode. Its request is{version, kind, icons, sourceBytes, text, value}:versionis required and is0(Core accepts only0, otherwiseUnsupported editor action validation version.),kindisregistration,actionsorinvokeResult, andkind: actionsalso needs the host'siconsandsourceBytes(theactionIconsand source bound it reports). It admits alternatives and click results (kind: invokeResult). Cases:fixtures/editor/editor-actions.json. Public host failures use the distinct codes (invalidParams,permissionDenied,versionConflict, etc.). No tool-private error or result is interpreted as a build/test result. commands/didRun {commandID,context?}is the command notification. A command-palette entry can read the document/selection and call the same plugin implementation.- A host that does not list
lithe/host/editor/registerActionsingetCapabilitiespresents no editor actions, so a plugin keeps its command/window entry point. A package requiringeditor.actionsis refused by a host that does not recognize that permission, rather than silently granted. Grammar capability admission is separate.
A provider adds a language feature the file's language server does not supply: a cross-file fact only the plugin knows, such as a key in a template that names an entry in a message bundle, or a framework declaration in a configuration file that names a method in source. It never replaces the language server. Where a language server answers, its answer comes first and plugin results only add to it; a plugin that repeats what the server already says shows the user the same thing twice.
A backend declares backend, documents.read and editor.providers,
finds lithe/host/editor/registerProviders and
services.editor.options.providerTypes with getCapabilities, then registers
per workspace context:
{
"context": {"contextID": "w1", "generation": 1},
"id": "message-keys",
"selector": {"languages": ["html"], "fileExtensions": ["properties"], "fileNames": []},
"providers": {
"definition": {"function": "keys.definition"},
"hover": {"function": "keys.hover"},
"completion": {"function": "keys.complete", "triggerCharacters": ["."]},
"semanticTokens": {"function": "keys.tokens",
"legend": {"tokenTypes": ["property", "string"], "tokenModifiers": ["declaration"]}}
}
}- Registration returns
{subscriptionID}; dispose it with the subscription API. Ending the session, the context or the plugin's activation withdraws every registration it made, and a plugin activated later registers again. Each named function must already be registered on that session. - A selector has 1 to 32 entries in total: language IDs, literal extensions (optional leading dot) and literal file names, matched case-insensitively. A document matches when any entry matches. At most 32 registrations per plugin across contexts; duplicate IDs in a context are refused.
providersmaps a provider type to{function}. The Lithe desktop host routesdefinition,hover,completion,codeLens,foldingRange,semanticTokensanddocumentLink. Completion may add 1 to 16 single-charactertriggerCharacters; semantic tokens must add theirlegend(at most 64 types and 64 modifiers). A type outside the host'sproviderTypesfails withunsupportedOptionanddetails.option: "providers.<type>"before anything is registered. Diagnostics are not a provider type.- The host calls the function through
lithe/function/invokewith the workspace context and{documentID,version}; definition, hover and completion addposition: {line,character}(zero-based, UTF-16), and completion addstriggerCharacterwhen typing started it. The plugin reads the text withdocuments/getSnapshotand answers for that version. - Results use the LSP 3.17 shapes of the matching request: definition
Location | Location[] | LocationLink[] | null,Hover | null,CompletionItem[] | CompletionList | null(nodata,commandor resolve round trip),CodeLens[] | null,FoldingRange[] | null,SemanticTokens | nullindexed by the registered legend, andDocumentLink[] | nullwith absolute targets. Every code lens carries itscommand, whosecommandis a function of the same plugin and whoseargumentsholds at most one params object; a click calls that function with{documentID,expectedVersion,data:<params>}. A malformed result is dropped whole. - Budgets are the host's: hover and completion 400 ms, definition 1500 ms, the rest 2000 ms. A late answer, or one for an older document version, is dropped without an error. A registration cannot set its own budget.
- The host merges: hover sections and completion items follow the language
server's; definitions add plugin locations to the server's (a location at
the requested position itself yields to the plugin's); code lenses, folding
ranges and links are added; semantic tokens apply only to files whose
language server has none. A plugin location equal to one already kept is
dropped. The merge is Core command
plugins.editorProviders.decodewithkind: "mergeDefinitions", so every host merges definitions alike. - Validation is shared Core command
plugins.editorProviders.decode. Cases:fixtures/editor/editor-providers.json. Example package:fixtures/editor/editor-providers.plugin.json. - A host that does not list
lithe/host/editor/registerProvidersingetCapabilitiespresents no editor providers. A package requiringeditor.providersis refused by a host that does not recognize the permission.
A plugin function is private to its plugin unless the plugin exports it. An
exported function can be called by another enabled plugin whose manifest
declares that call. Core relays every call: the calling backend never
reaches the provider's process, the provider's backend never learns more than
the call it serves, and the host takes part only by starting a provider's
backend that is not running (see below). The relay is Rust Core logic, the same
on every platform that runs plugin backends.
fixtures/backend/plugin-functions-exporter.plugin.json and
fixtures/backend/plugin-functions-caller.plugin.json are the examples.
"requires": { "pluginFunctions": 0, "backend": 0 },
"pluginFunctions": {
"exports": [{ "id": "document.format", "version": 1, "context": "workspace" }],
"calls": [{ "id": "project.describe", "version": 2 }]
}pluginFunctionsneeds abackend, andrequireslistspluginFunctions: 0andbackend: 0. The section holds at least one ofexportsandcalls, each at most 32 entries sorted byid, with noidtwice. Anything else, including an unknown field, isplugin.manifest.invalid.requireslistspluginFunctionsexactly when the section exists.- An export is an object, never a bare function ID.
idis a function ID as in References and IDs.versionis the exported contract version, an integer from 1 to 1000; an incompatible change to the params or result takes a new version.contextis a closed set that holds only"workspace": the function runs only for a local workspace context, on a call Core relays. A missing or unknowncontextrefuses the whole manifest withplugin.manifest.invalid, so a function whose context is unstated is never callable. - A call is
{ id, version }. It names a function ID and the version the caller was written against, never the providing plugin's ID: the caller discovers providers at run time. Core requires the versions to be equal; otherwise the call fails withplugin.function.versionMismatch. - Declaring an export does not register it. The provider's backend still
registers the function with
lithe/function/register, and a function it has not registered, or has withdrawn, isplugin.function.notExported. A function the manifest does not export is private whatever the backend registers, and a reference to it keeps answeringplugin.function.wrongPlugin. - A plugin that exports needs no page, command or
onWorkspaceOpento start its backend: a call starts it. A plugin that only calls needs one of those, as everybackenddoes. pluginFunctionsgrants no manifest permission. The caller'scallsand the provider'sexportsare the two sides' consent, and the host grants a plugin that declarescallsthefunctionshost service, throughbackend.hostServicesof its launch plan, like every other service. A host withoutpluginFunctionsinhostCapabilitiesrefuses such a manifest withplugin.api.unsupported.
- Trust is not transitive. The provider runs with its own permissions,
its own state directory and its own credentials. The caller gains none of
them, and the provider gains none of the caller's. Core never writes the
caller's plugin ID into the provider's session; it appears only in the
callerfield of one request, below. - The provider owns its resources and processes. An exported function must not accept an executable path, command line or script from its caller and start a process with it, nor read or write a path the caller chose without checking it against what the provider itself may access. If a caller needs the provider to run something, the function takes a request the provider interprets (a task name, a document) and the provider decides the executable.
- A function that decides by who asks reads the
callerfield below, never a plugin ID inparams, which any caller can write.
A backend whose plugin declares at least one call may send two requests; a
plugin that declares no call gets -32010, plugin.host.permissionDenied.
getCapabilities lists the methods the session may call under the functions
service.
| Request | Params | Result |
|---|---|---|
lithe/host/functions/providers |
{ functionID } |
{ providers: [{ pluginID, version }] } |
lithe/host/functions/invoke |
{ pluginID, functionID, context: { contextID, generation }, params?, timeoutMilliseconds?, parentOperationID? } |
{ value } |
providerslists the enabled plugins other than the caller that exportfunctionID, sorted by plugin ID, each with its exportedversion. Disabling, enabling, replacing and uninstalling a plugin take effect for the list at once; a host that registers backend plugins only when it launches lists a newly installed or updated backend plugin only after a restart of Lithe (see Hot activation). The caller must have declared the function, otherwiseplugin.function.notDeclared. A listed provider's backend need not be running.invokecalls one provider.contextis required: a workspace context the caller learned from the workspace service, checked like every call context.paramsis a JSON object and defaults to{}.timeoutMillisecondsis a positive integer, defaults to 60 000 and is capped at 600 000. A field the method does not define is-32018,plugin.host.unsupportedOption. The reply is{ value }, wherevalueis the provider's result (nullis a result).parentOperationIDis optional: a string of 1 to 128 bytes, otherwise-32602,plugin.host.invalidParams. A backend that makes a nested call while it serves a relayed call sets it to theoperationIDof thelithe/function/invokeit is serving (the field shown in The caller field), so Core knows which call the new one belongs to. Its effects are described in The call chain. A backend should always set it: without it Core treats the new call as a child of every relayed call the backend is serving.- The caller cancels a call with
$/cancelRequestfor itsinvokerequest id; it fails withplugin.host.cancelled.
Core checks, in this order, and refuses a call before anything is written to the provider; the first failing check wins:
| Check | Code |
|---|---|
| The caller declared the function | plugin.function.notDeclared |
| The provider is installed and enabled | plugin.function.providerDisabled |
| The provider exports the function | plugin.function.notExported |
| The declared and exported versions are equal | plugin.function.versionMismatch |
| The context is open, and has this generation | plugin.host.contextExpired |
| The context is a local workspace | plugin.host.authorityUnsupported |
| The params are at most 256 KiB | plugin.function.payloadTooLarge |
The call named by parentOperationID is still running |
plugin.host.cancelled |
| The provider is not on the call chain, the caller included | plugin.function.callCycle |
| The chain has at most 4 calls | plugin.function.depthExceeded |
| The caller has at most 8 relayed calls running | plugin.function.tooManyCalls |
A provider's own failure keeps its meaning. The reply is a JSON-RPC error
with data: { "code", "details" }; when the provider's function answered with
an error, code is plugin.function.failed (-32070), message is the
provider's message cut to 1024 bytes, and details.pluginErrorCode is the
provider's own error.data.code. A reply with no result, or with one over
1 MiB serialized, is plugin.function.invalidResult (-32071); any other
result over 256 KiB is plugin.function.payloadTooLarge. Neither number is
-32603, which stays plugin.host.failed, a failure inside Core. The relay's deadline passing is
plugin.host.timeout. A call that is running normally ends through the
provider session's own timeout; if the provider has still not answered 2
seconds after the deadline, the relay ends the call with
plugin.host.timeout itself and cancels the provider's operation. A
provider whose backend exits, or that withdraws
the function, while the call runs ends it with
plugin.function.providerUnavailable; a provider that is disabled or
uninstalled, or whose update stops exporting the function or changes its
version, ends it with plugin.function.providerDisabled. A caller whose
update stops declaring the call ends it with plugin.function.notDeclared.
A workspace context that the host closes, or replaces with a newer
generation, ends the relayed calls that carry it with
plugin.host.contextExpired: both the calls still waiting for a provider and
the calls already running, whose provider also receives $/cancelRequest.
Core never restarts a backend or retries a call to finish one.
fixtures/backend/function-protocol.json holds the cases: the host requests
(hostRequests), the numbers of every failure (relayErrors) and the
caller field (invocationCallers).
Core records the chain a call belongs to. A provider that makes a call while it
serves a relayed call names that call in parentOperationID. The new call is
then a child of that call only:
- Its chain is the chain of that call plus the caller.
- Cancelling that call, its timing out, or its ending cancels the new call.
A call to a plugin already on the chain, including a plugin calling itself, is
plugin.function.callCycle: the plugin on the chain is waiting for the call
that would reach it. A chain has at most 4 calls. So a provider that is asked
to apply values must not call back the plugin that asked inside that call; it
records the values, or calls after the call ends.
- If
parentOperationIDnames a call the host started directly, not a relayed one, the new call starts a new chain. - If it names a call this backend is not serving (one that has finished, or
one that never existed), the call is refused with
plugin.host.cancelledand the message "The call this request belongs to is not running." - If it is omitted, the call counts as a child of every relayed call the backend is serving at that moment; SDKs should always fill it in.
Cancelling a call, its timing out or ending, or the end of its caller's
session, cancels the calls made as its children, down the chain, and each
provider receives $/cancelRequest. The relay never retries a call.
Core writes to each backend session through a queue of its own. A backend
that stops reading its standard
input only delays the calls and replies sent to it; the relayed calls of other
plugins continue. If a write to a backend stays blocked past the request
timeout, Core ends that backend's session as a transport failure
(transportFailed, stage relayWrite), and the calls of that session end like
those of any session failure.
For every relayed call Core forwards lithe/function/invoke to the provider
as for any call (see Invocation) and adds one top-level field:
{
"functionID": "project.describe",
"operationID": "7",
"context": { "contextID": "w1", "generation": 1 },
"params": { "project": "app" },
"caller": { "pluginID": "dev.lithe.fixture.python" }
}caller.pluginIDis written by Core from the identity of the calling session. It is a sibling offunctionID,operationID,paramsandcontext, never insideparams, and the caller cannot supply it: alithe/host/functions/invokerequest carrying a top-levelcalleris refused with-32018,plugin.host.unsupportedOption. Acallerkey inside the caller's ownparamsis ordinary data and is passed unchanged.- A call the host itself started has no
callerfield. Absent means the host. - A provider that keeps per-caller state (for example a slot per calling plugin) keys it by this ID, which cannot be forged.
If the provider's backend is not running, Core waits for it and emits the
runtime event functionProviderStartRequired with
result: { "pluginID" } on the calling session, once per waiting provider.
The host starts that plugin's backend as an ordinary plugin start, and Core
dispatches the call when the provider has negotiated the protocol and registered
the function. If that takes longer than 30 seconds the call fails with
plugin.function.providerUnavailable; the relay's deadline still applies.
A host that cannot start the backend ignores the event, and the call ends the
same way.
The relay is a local-workspace feature. A call whose context is a remote
workspace is refused with plugin.host.authorityUnsupported, and the
exported context value "workspace" means a local workspace only. Backends
still run on the machine that runs Lithe.
Disabling, enabling, replacing and uninstalling a plugin take effect for the
relay at once and need no restart of Lithe or of any other plugin.
The host sends the plugin and workspace-context snapshot with the Core
command lsp.configureFunctionRelay
(Rust Core API),
and calls the new snapshot no longer allows end at once with the errors above.
Installing or updating a plugin that has a backend is different on a host that
registers backend plugins only when it launches: that host lists
a newly installed or updated backend plugin in providers only after a
restart of Lithe.
- Private functions add no manifest field.
pluginFunctionsis a capability (requires.pluginFunctions: 0), so only a plugin that exports or calls functions is refused by a host that cannot relay them. - A backend that does not answer
litheFunctions(a build-only backend) keeps serving its language and itslitheBuildrequests and receives no function calls; its function requests are answered with-32601. - A new host capability or optional field is added within the version
(Extending the host services); only a change
an existing backend would misread gives
backendversion 1, and the protocol moves with it. fixtures/backend/function-protocol.jsonholds the golden cases: host requests (hostRequests), call contexts (invocationContexts), the caller of a relayed call (invocationCallers), host failure codes (hostFailures), relay failure codes (relayErrors), and host notifications (hostNotifications). Rust Core tests run them, and a scripted backend covers routing, result, failure, cancellation, deadline, late reply, revocation, and a build-only backend.
The official SDKs live next to this contract in
shared/plugins/sdk/ and are released from this repository;
they implement this contract and never define protocol.
sdk/node is the official SDK for backends
started through a node runtime launch. It implements this protocol with
its host services, litheBuild, and VCS providers, so a plugin author writes
only handlers. It is released (sdk-node-v<version>) as a .tgz with its
SHA-256 for plugins to vendor into their package. It enforces the rules above on the backend side: lithe/* methods are never sent, stdout
carries only protocol frames, PluginError is the only way to set a
pluginErrorCode, processes started for a call end with it, and its state
store refuses the plugin package.
sdk/java is the Java twin for backends
started through a java runtime launch. It implements this protocol with
its host services and litheBuild, but not VCS providers. It needs Java 8 or
newer, has no runtime dependencies, and is released as a JAR with its SHA-256
for plugins to shade into their backend. It enforces the same rules with the same error
mapping, with one difference: on Java 8 it stops only a cancelled tool's
direct child, and the host's process scope ends the rest.
sdk/rust is the SDK for
plugin backends, which ship as packaged executables. It
implements this protocol with its host services, but neither build nor VCS
providers. It needs Rust 1.87 or newer, depends only on serde_json, and is
released (sdk-rust-v<version>) as a .crate archive with its SHA-256, or
used as a Git dependency pinned by the tag. It enforces the same rules: on Unix it
points stdout at stderr so only protocol frames reach the host, and a panic
ends only its call, reported like any unexpected failure.
The Node.js and Java SDKs each have a sample build provider
(sdk/*/examples/sample-build) that answers one end-to-end scenario driven
through a real Core session; the Rust SDK's sample backend
(sdk/rust/examples/sample-backend) answers a plugin backend scenario in which
the test plays the host: it serves the credential requests and the
workspace, file watch and window requests, delivers a command and a context
change, and checks that cancellation reaches the host. That repository runs
them against the Lithe commit its contract copy pins, and weekly against the
latest development branch of Lithe.
No SDK adds a wire field: a backend written without one is equally valid.
plugins.declarative.*, plugins.marketplace.*, build.*, vcs.*, and project.* commands return {"status": "ok", …} or
{"status": "rejected", "error": {"code", "message", "field"?}}. A malformed
command payload is a Core invalid_request failure instead.
| Code | Reported by | Meaning |
|---|---|---|
plugin.manifest.invalid |
Core | Malformed manifest, unknown field, or broken cross reference |
plugin.schema.unsupported |
Core | schemaVersion is not 0 |
plugin.api.unsupported |
Core | pluginAPIVersion, or a capability version in requires, is not supported by this Core build or, when the host sent hostCapabilities, by the host |
plugin.permission.unknown |
Core | Unknown permission |
plugin.permission.undeclared |
Core | A provider or build system is declared without its permission |
plugin.activation.invalid |
Core | Missing or orphan activation event |
plugin.runtime.undeclared |
Core | A tool that a launch, a runtime dependency or a debug adapter names and requiresTools does not list; field names where it is used |
plugin.run.invalidStart |
Core | The params of lithe/host/run/start are malformed, name a tool the manifest does not require, or leave the workspace or package; field names the part |
plugin.debug.invalidStart |
Core | The params of lithe/host/debug/start are malformed, or configuration holds a key the host reserves; field names the part |
plugin.debug.unknownAdapter |
Core | lithe/host/debug/start names an adapter the plugin does not declare |
plugin.debug.foreignAdapter |
Core | lithe/host/debug/start names an adapter of another plugin |
plugin.debug.limitExceeded |
Core | A debug/start configuration is larger than 256 KiB |
plugin.toolchain.launchMismatch |
Core | A tool definition's launch template cannot carry the entry, runtime arguments or arguments a launch supplies, or needs a part the launch left out |
plugin.template.invalid |
Core | Unknown, misplaced, or unguarded template variable |
plugin.path.invalid |
Core | A request path leaves the workspace, or a declared path leaves its base directory |
plugin.test.invalidIdentifier |
Core | Empty or multi-line test identifier |
plugin.test.unsupportedProject |
Core | No project marker in the workspace |
plugin.execution.unavailable |
Core | The plugin has no run/test provider for the language |
plugin.integration.unsupported |
Core | A hostIntegration ID; Core implements none |
plugin.launch.unavailable |
Core | The plugin has no language server for the language, no provider process for the build system, or no such VCS provider |
plugin.build.unavailable |
Core | The plugin declares no such build system, or the build system does not declare the requested operation |
plugin.build.invalidSelection |
Core | A build request selects a scope, target, filter, or argument the declaration does not allow, or misses a required path |
plugin.build.invalidOutput |
Core | Build-tool output or a report does not fit its decoder |
plugin.launch.invalidRequest |
Core | A launch plan request has an unusable directory or an unknown architecture |
plugin.launch.notBackend |
Core | decodeLaunch named a language server that the plugin backend does not launch |
plugin.launch.invalidResult |
Core | A backend-resolved launch answer is malformed, unknown in a field, or over a bound |
plugin.launch.outsidePackage |
Core | The answer's packaged executable leaves the plugin package |
plugin.launch.undeclaredTool |
Core | The answer's tool is not one of the plugin's tool definitions |
plugin.launch.unwritablePath |
Core | The answer writes outside the state, workspace state, log, and temporary directories |
plugin.grammar.invalid |
Core | A language's grammar file is not a TextMate grammar Core accepts; field names the failing part |
plugin.grammar.unavailable |
Core | The plugin declares no grammar for the language |
plugin.settings.invalidLocalization |
Core | A translation file does not fit its manifest or format; the host shows the next candidate or the manifest text |
plugin.vcs.reserved |
Core | A VCS provider claims the ID of the built-in Git support |
plugin.vcs.invalidOutput |
Core | A VCS provider's reply does not fit the VCS provider contract |
plugin.index.invalid |
Core | Malformed index root or entry, or an invalid URL, checksum, platform, version, or date |
plugin.index.schemaUnsupported |
Core | The index schemaVersion is not 0 |
plugin.index.officialNamespace |
Core | A third-party source lists a dev.lithe ID |
plugin.index.duplicate |
Core | An index lists the same plugin ID more than once |
plugin.index.platformUnsupported |
Core | No version has an archive for this platform (availability, not a rejection) |
plugin.index.apiUnsupported |
Core | Only versions needing a plugin API or a capability version the host does not run list this platform (availability, not a rejection) |
plugin.source.invalid |
Core | A marketplace source URL cannot be used |
plugin.package.mismatch |
Core | A downloaded package's ID or version differs from its index entry |
plugin.archive.checksumMismatch |
Host | A downloaded archive does not match the index SHA-256 |
plugin.download.failed |
Host | An index or archive could not be downloaded |
plugin.signature.missing, plugin.signature.invalid, plugin.signature.untrusted |
Host | Package signature problems |
plugin.launch.missing |
Host | The packaged language server is missing or not executable |
plugin.runtime.missing |
Host | A tool a launch needs cannot be found, or is older than its minimumVersion, when the server starts |
plugin.web.unknownMethod |
Host | A plugin page called a bridge method the host does not implement |
plugin.web.invalidRequest |
Host | A bridge call has malformed parameters |
plugin.web.forbidden |
Host | A bridge call names an instance, run, or path outside the page's plugin or workspace, or the page is closed |
plugin.function.invalidReference, plugin.function.invalidParams, plugin.function.unavailable, plugin.function.wrongPlugin, plugin.function.unknown |
Core | A plugin-function call is refused before it reaches the backend |
plugin.function.failed, plugin.function.invalidResult, plugin.function.revoked |
Core | A plugin-function call ended without a usable result |
plugin.function.invalidRegistration |
Core | A backend's register or unregister request was refused as a whole |
plugin.function.notDeclared, plugin.function.notExported, plugin.function.providerDisabled, plugin.function.versionMismatch |
Core | A relayed call is refused: the caller did not declare it, the provider does not export (or has not registered) the function, the provider is not enabled, or the declared and exported versions differ |
plugin.function.callCycle, plugin.function.depthExceeded, plugin.function.tooManyCalls, plugin.function.payloadTooLarge |
Core | A relayed call is refused by a bound: the provider is already on the call chain, the chain is too long, the caller has too many relayed calls running, or the params or result are too large |
plugin.function.providerUnavailable |
Core | The provider's backend did not start in time, exited, or withdrew the function while a relayed call waited for it |
plugin.project.unavailable |
Core | The plugin declares no such project generator |
plugin.project.invalidInput |
Core | The project name or a field value is not accepted; field is projectName or values.<id>, or a step whose expanded path leaves the project |
plugin.project.templateInvalid |
Core | A template directory is missing, too large, contains a symbolic link or non-UTF-8 file, or uses an unknown variable |
plugin.project.invalidURL |
Core | An archive address does not expand to an HTTPS URL (HTTP only for loopback hosts) without credentials, query, or fragment |
plugin.project.archiveInvalid |
Core | A downloaded archive is not a zip, has an entry outside the destination, a symbolic link, a file that already exists, or expands beyond the size limit |
plugin.project.generationFailed |
Host | A command step failed or timed out, a download failed, or the project could not be moved into place |
plugin.function.recursiveCall |
Core | A backend asked the host to dispatch a domain operation or function |
plugin.host.unavailable |
Core | A backend asked for a host method the protocol does not define |
plugin.host.permissionDenied |
Core | A backend asked for a host service its plugin's permissions do not grant |
plugin.host.invalidParams |
Core | A host-service request has malformed params |
plugin.host.unsupportedOption |
Core, Host | A host-service request names a field the host does not implement; details.option names it |
plugin.host.failed |
Core | The host could not complete a host-service request |
plugin.host.contextExpired |
Host | The request names a workspace context that closed or has a newer generation |
plugin.host.authorityUnsupported |
Host | The context's authority (for example a remote workspace) does not implement the operation |
plugin.host.versionConflict |
Host | A versioned write names an older version than the current one |
plugin.host.limitExceeded |
Core, Host | A host limit on size, count, or rate was exceeded |
plugin.host.cancelled |
Core, Host | The backend cancelled the host request, or the host stopped it |
plugin.host.timeout |
Host | The host's own deadline for the operation passed |
plugin.host.notFound |
Host | The named resource does not exist |
plugin.host.toolUnresolved |
Host | A tool definition the request names resolves to no executable; details is { tool, reason } |
plugin.host.readOnly |
Host | The resource or workspace cannot be written |
plugin.host.preconditionFailed |
Host | A state the request requires does not hold |
plugin.host.busy |
Host | The resource is in use; retry later |
plugin.host.rateLimited |
Host | Too many requests in a period |
plugin.host.languageServerUnavailable |
Core | The plugin's language server is not running for the context, or stopped while a request was in flight |
plugin.host.languageServerError |
Core | The language server answered a forwarded request with an error; details is {code, message} |
plugin.host.methodNotDeclared |
Core | A forwarded method or command is not in the server's declared session |
plugin.host.foreignContribution |
Core | The named language server belongs to another plugin |
plugin.backend.exited |
Host | A page's call, or a host operation for the backend, ended because the backend process exited |
Host-facing plugin states are notInstalled, installed (enabled or
disabled), running, restartRequired, updateRequired, and failed.
ui contributions need requires.ui: 0 and the ui.contribute permission.
UI-only packages may omit languages. See fixtures/manifest/hello-lithe.plugin.json
and the UI cases in fixtures/manifest/invalid-manifests.json.
ui.toolWindows: 1–64 entries with stableid,title, initialplacement(left,bottomorright), integerorder(default 0), and plaintext.ui.commands: up to 128 entries with stableid,title,toolWindowID, integerorder(default 0), and nonemptyplacements(commandPalette,toolbar,menu). A command opens one of the same plugin's tool windows.- IDs use the plugin ID followed by a dot and a suffix. Duplicate or foreign
identities, invalid references, unsupported placements and executable UI
fields are rejected with
plugin.manifest.invalid. - Titles are at most 256 UTF-8 bytes and window text at most 65536 UTF-8 bytes.
Text is displayed literally; it is never evaluated as HTML or a script.
A window shows exactly one of
text,webandnative. - Declare
onView:<window-id>andonCommand:<command-id>for every window and command, plusui.contribute. The existing permission and activation error codes apply. IDs and presentation order are normalized deterministically. - The host exposes metadata without instantiating the module. Opening an entry activates the plugin's UI module on demand; a disabled, quarantined or safe-mode-suppressed module has no executable UI entries. Disabling it removes the entries and closes its visible content.
- The host owns header controls, widgets, menus, theme, ordering and layout. Saved workspace preferences override a declaration's initial placement and order. Moving or hiding content does not create a new module or session. Unknown and temporarily absent contributions never execute an action; placement/order preferences remain available if the plugin returns.
A host may support mouse docking through the existing shared dock state; that presentation behavior does not grant additional plugin execution capabilities.
A plugin does not have to bring a UI: build systems get a host-drawn window
(see Presentation), and text windows need no code. A plugin
that wants its own UI can show its own page in a tool window instead of
text:
"requires": { "ui": 0 },
"permissions": ["ui.contribute", "ui.web"],
"ui": { "toolWindows": [{ "id": "…", "title": "…", "placement": "right", "web": { "entry": "ui/index.html" } }] }webneedsrequires.ui: 0and theui.webpermission, which the install review lists like every permission.entryis a package-relative.htmlpath.fixtures/ui/web-panel.plugin.jsonis the example; the web cases offixtures/manifest/invalid-manifests.jsonare the rejections.
The window shell stays the host's: dock, title, activity-bar entry, order, visibility, layout, and lifecycle are declared or managed exactly as for a text window. The page fills the body only and cannot open, place, size, or title a window.
Hosts load a page under these rules:
- Only files of the installed package are served, read-only, from an origin
private to the plugin. The host's content security policy refuses network
connections, remote resources, frames, inline scripts, and
eval. Navigation away from the package, popups, and downloads are blocked. - Web storage is not persistent. A page keeps its state through the bridge.
- The package path
.lithe-host/is reserved: the host answers.lithe-host/bridge.jswith its own bridge script and serves no package file under it. - The host creates the page when the window is first shown. It may destroy the page whenever the window is hidden, and does destroy it when the plugin is disabled, updated, or uninstalled. A page restores itself from its state.
- The root element carries
data-theme(lightordark) and the host's--lithe-*CSS colour and font variables, updated with the theme.
Before the page's scripts run, the host defines window.lithe:
lithe.version, the bridge version (0), lithe.context (pluginID, windowID, platform, locale,
theme), lithe.request(method, params), which returns a promise of the
result or rejects with { code, message }, and lithe.on(event, listener),
which returns an unsubscribe function. A page reaches only its own plugin's
build systems and the open workspace. Requests that run anything ask for the
same per-operation consent as the host's own controls (Lithe has no workspace
trust state at the moment), and
their output appears in the host's Build output window.
| Method | Parameters | Result |
|---|---|---|
build.instances |
none | { instances: [{ id, buildSystemID, root, state, model?, options, error? }] }: this plugin's detected instances, model as build.decodeModel returns it, options the stored values |
build.refresh |
{ instance } |
{}; reloads the model |
build.runTask |
{ instance, task, project?, target?, modelTask?, userArguments? }, the selection of build.taskPlan |
{ run } |
build.runTests |
{ instance, project?, target?, suite?, case? }, the selection of build.testPlan |
{ run } |
build.cancel |
{ run } |
{} |
build.dependencies |
{ instance, project? } |
the tree build.decodeDependencies returns |
build.setOptions |
{ instance, values } |
{ options }, checked by build.resolveOptions and stored like values set in the host's option list |
workspace.openFile |
{ path, line?, column? }, a path inside the workspace |
{} |
ui.getState, ui.setState |
none, { state } |
{ state }, {}; JSON of at most 64 KiB per window and workspace |
functions.list |
{ instance? } |
{ functions }: sorted IDs of the functions the instance's provider, or without instance the plugin's backend, currently offers; starts that process |
functions.invoke |
{ instance?, function, params?, call? }, without instance a call to the plugin's backend; params an object, call a page-chosen ID of at most 128 characters that the page never used before |
{ value }, the function's result |
functions.cancel |
{ call } |
{}; a call still awaiting consent never starts, a call waiting for its provider to start ends at once while the start goes on, and a call that already ended is ignored |
dialog.openFile |
{ title?, extensions? } |
{ path }: the absolute path of the file the user chose, or null when the dialog was cancelled |
dialog.saveFile |
{ title?, suggestedName?, extensions? } |
{ path } as for dialog.openFile; the host has asked before replacing an existing file, and nothing is written |
| Event | Payload |
|---|---|
build.instancesChanged |
{ instances }, as build.instances returns them |
build.runChanged |
{ run, instance, state, exitCode? }, state one of running, succeeded, failed, cancelled |
functions.progress |
{ call, instance?, function, message?, percentage? }, only for a call made with call; instance absent for a backend call |
ui.themeChanged |
{ theme } |
The file dialogs are the host's: they name the plugin that asks, a page
has at most one open at a time, and the page learns only the path the user
chose. extensions are up to 16 file name extensions without the dot;
suggestedName is a file name, not a path. A page passes the path to its
backend, which reads or writes the file; the page itself still reaches no
file.
Bridge errors are plugin.web.unknownMethod, plugin.web.invalidRequest,
and plugin.web.forbidden. Build requests also report the build.* codes.
Function requests report Core's codes unchanged (plugin.function.*,
requestCancelled, requestTimeout); a failure the plugin reported also
carries its own pluginErrorCode, which the host never interprets. A call
Core could not deliver, such as a write to a backend that exited, keeps
Core's transport code (processFailed) and carries the OS diagnostic only in
its message. A backend that stops reading its input is ended when a write to
it stays blocked past the call's timeout, or briefly after the call was
cancelled or the provider stopped. A page
has at most 16 function calls accepted at once, counting calls still awaiting
consent, and closing it cancels them. Reusing a call ID is
plugin.web.invalidRequest, also after that call ended.
functions.list and functions.invoke with an instance ask for the same
consent as the host's provider requests: once given for a plugin and
workspace, it holds until the project is closed. Calls to the plugin's
backend ask for none; the install review covered it.
The community index is Lithe Store. To add a host-rendered view to a community package:
- Add
uitorequires,ui.contributeto the permissions, and the newonView/onCommandevents to the activation events. - Add
ui.toolWindowsandui.commands; prefix every ID with the plugin ID, for exampleio.github.lithe-idea.lua-support.tools. - Keep
vendor.signatureRequirement: "none"for community packages. The existing package checksum, user install review and content digest still apply; UI does not bypass them. - List the platforms whose hosts declare every capability the package requires; the index schema, release archive format and SHA-256 rules do not change.
fixtures/manifest/hello-lithe.plugin.json is a contract fixture in the reserved test
namespace, not a ready-to-publish community package. Change its ID and all
references to your own namespace before using it in Lithe Store.
Tool windows and commands accept an optional icon. Prefer a stable
host icon ID; it resolves through Lithe's existing icon library, not SF Symbols:
"icon": { "host": "database" }Supported host IDs: tool-window, plugin, database, services, terminal,
search, refresh, run, debug, settings, add, remove, git, problems.
These IDs are public; bundled SVG paths remain implementation details.
Custom SVG and PNG icons are relative to the plugin package root:
"icon": {
"path": "icons/database.svg",
"darkPath": "icons/database-dark.svg",
"mode": "template"
}mode defaults to template: the host tints the image using the containing
control's theme and selected/hover state. original preserves brand colors;
control backgrounds and disabled opacity still follow the host. darkPath is
optional; without it the same resource is used in both themes. Brand authors
must make that resource legible in both themes or provide a dark variant.
host cannot be combined with resource paths or original-color mode.
The host owns icon size: 20pt tool-window rail and 16pt actions/menus/palette. Panel titles are text-only: no title icon, outline or bottom divider. A declared window icon remains available in the activity rail and management menus. Aspect ratio is preserved inside each icon slot. Plugin-specified size, color, stroke, arbitrary style, remote URLs and SF Symbol names are rejected. Stroke geometry belongs to the asset; authors should follow the host icon weight, rather than expecting the host to rewrite paths or PNG pixels.
Window and toolbar management submenus reuse the same declared icons at the host menu size. Template icons in the command palette use the host selected-text token on the selected row; brand icons retain their theme variant when keyboard selection moves.
Files must be regular files inside the verified package, without symbolic links in their path. Each resource is limited to 256 KiB and 1024×1024; unique icon bytes per package are limited to 8 MiB, with at most 4,194,304 total image pixels. PNG is decoded as a single image. SVG accepts static shapes, groups, gradients, clip paths and the standard SVG version attribute; scripts, event handlers, external references, embedded images, style elements/attributes, entities and DOCTYPEs are refused. Use explicit presentation attributes for SVG colors. Unknown fields and unsupported formats are rejected at installation and launch.
Window entries without an icon use the shared generic tool-window icon. Commands without an icon remain text actions (palette retains its existing action marker). Missing/unreadable declared resources reject the package instead of silently using a different image. Assets are read-only and held in process memory; no runtime disk cache or app-bundle write is created. Installed icon bytes follow the existing package signature/digest, update and rollback lifecycle.
Lithe 桌面端布局菜单允许把已注册工具窗口移到左侧、右侧或底部,并保存入口
顺序与显隐;ui.toolWindows[].placement 只声明首次使用的 bottom/right
位置。项目、提交和搜索采用相同的工具窗口注册路径,
原生业务内容保持宿主所有;左侧恢复插件窗口仍按 onView 激活所属模块。
Lithe 桌面端动作的可见、可用、勾选和激活中状态由宿主计算,不接受清单中的可执行 条件脚本。窗口尚在激活时,其打开命令在工具栏、菜单和命令面板禁用;窗口入口 仍可取消显示。多个窗口同时请求同一插件模块时只构造一次;禁用或关闭会取消 并清理在途激活,旧的菜单或命令引用不能重新调用已禁用的贡献。
A tool window whose sole body is native is rendered by the host from a
declarative document; the manifest requires ui: 0. The independently
versioned native schema (schemaVersion 0) has no
style, script, HTML or host-action fields. The host renders every control with
its shared theme.
See native-panel.plugin.json and the runnable
Node SDK Sample Explorer.
native.kind is tree, list or form; nodes contains literal id, label and
optional children. IDs are stable, case-sensitive ASCII letters/digits plus
._- (1–128 bytes). The component, initial nodes and actions have distinct IDs.
The entire node set is limited to 2000 nodes and depth 16 (root depth 0); labels
are nonblank and at most 1024 UTF-8 bytes. Lists reject nested children.
A form declares nodes: [], an empty contextMenu, at most 32 controls
and 16 buttons; at least one control or button is required.
Tree/list panels cannot contain controls. Each control has a unique id, literal
label (1–256 UTF-8 bytes), kind, initial value, optional enabled (default
true), and optional onCommit referencing a declared invoke action that does
not require a selection. An optional literal info (1–1024 UTF-8 bytes) renders the shared information
icon beside the label; tooltip timing, pinning, accessibility and styling are host-owned.
input/search: stringvalue(at most 1024 UTF-8 bytes), optionalplaceholder(at most 256 UTF-8 bytes), no options. Uses the IDE settings text-field owner.select: stringvalueequal to one of 1–128 uniqueoptions: [{id,label}]; option IDs use the standard identifier rule, labels are 1–256 UTF-8 bytes. No placeholder. UsesLitheSettingsSelect, including its popup and keyboard behavior.segmented: string value and 1–8 options, using the same option contract and the shared segmented selector; no placeholder.editor: string value up to 8192 UTF-8 bytes, optionalrows(2–12, default 4), no placeholder/options/onCommit. Reuses the shared multiline editor; Enter inserts a newline and form buttons submit its current value.stepper: finite numeric value, requiredminimum,maximumand positivestep; bounds and step are limited to ±1e9/1e9, and value must fit the range. No placeholder/options. Uses the shared settings stepper and bound behavior.checkbox: booleanvalue, no options or placeholder. UsesLitheSettingsCheckbox.
Forms may declare layout: {"columns":12} (1–12 columns). Each control may
provide span (1–columns); omitted span fills a row. Controls pack in declaration
order, wrapping before a span would exceed the row. Without layout, the form
retains its original single-column presentation and must omit span. Layout is
form-only. Gaps, alignment and breakpoints remain host-owned; plugins cannot
supply pixels, CSS or gap values. Reflow preserves the same controls, local values and focus.
Buttons reference an existing non-selection action through action, have stable
id, literal label, optional primary, enabled and info.
Text actions reuse the host's shared natural-width button style and 6pt action
spacing; a narrow action group falls back to vertical stacking.
buttons[].width (content/fill) and buttons[].span are accepted but do
not change button width or action-group layout.
Grid spans continue to apply to value controls. The declaration cannot override
button colors, fonts, chrome, padding or fixed/max widths.
Input/search editing saves local values without executing plugin code; Return commits.
Selectors, segmented selectors, steppers and checkboxes commit after a changed value. Omit onCommit to keep
a field local. Commits pass all current fields as params.values (strings,
booleans and numbers keyed by stable control ID), together with selectedID: null.
The subsequent form source call also receives {values} and returns {nodes: []}.
Disabled fields still contribute their values; they cannot edit or commit.
A pending call disables all fields. Values use the existing 64 KiB per-plugin,
per-window, per-workspace state store; removed fields, invalid types and removed
options are discarded when restoring. Provider replies never overwrite local
fields. Form declarations and choices are static.
See native-form.plugin.json for acceptance.
An optional source names this plugin's buildSystemID and a function returning
{ nodes: [...] }. The source uses the existing provider transport; it adds no
backend, provider registry, or process protocol. There must be exactly one
detected instance of that build system in the workspace; zero or multiple
matches produce an error instead of silently targeting a different root.
Static panels omit source and declare empty actions and placements.
actions declares at most 64 independent handlers with id, literal title,
kind (refresh or invoke) and requiresSelection. A refresh reloads the
source and cannot require selection or name a function. An invoke names a
function and receives { selectedID: string | null } (forms also include values). Invocations return an
optional { message }, then the host reloads the source. toolbar and
contextMenu contain distinct references to declared action IDs in display
order. Toolbar actions are icon-only and appear when the pointer enters the whole
panel, like the host hide control; keyboard and accessibility focus also reveal them.
An optional icon: {"host":"search"} reuses the host icon vocabulary (including
reset); native actions do not load custom package icons. Without an icon, refresh
uses the refresh icon and invoke uses the generic run icon. The literal title is
the tooltip and accessibility name. A context-menu action selects its clicked row before running; selecting
a row alone never starts a provider function. These are panel-local actions;
they do not turn ui.commands into arbitrary function handlers.
The host allows one operation per panel, disables actions while busy, and shows loading, empty, failure and success feedback. A source reply is at most 1 MiB and follows the same node limits; invalid data preserves the last valid rows. Selection and expansion use stable IDs and the existing per-plugin, window and workspace state store (at most 64 KiB). Arrow keys move selection, expand/collapse nodes and select a parent; hidden/reopened panels restore state. Provider calls retain existing consent, scope checks, timeout and cancellation. Hiding a native panel cancels its pending operation. Disable, package replacement and workspace close synchronously invalidate it through the existing shared plugin-page lifecycle; late replies cannot publish. All state is outside the installed package and application bundle.
A refresh loads the complete node set. Lazy child data sources, multi-instance selectors, global function commands and native renderer factories are not part of the native schema.