插件后端是插件自己的长驻进程,用来运行网页做不到的事:数据库驱动、网络客户端、文件格式解析、调用宿主服务。它只服务插件自己,不属于任何构建系统、语言或工作区,也不回答 lithe/build/*、VCS 等领域请求。后端与宿主之间说插件函数协议(litheFunctions),可以按权限请求宿主服务,可以启动由宿主拥有的 Run 会话和调试会话,也可以经 Core 调用其他插件导出的函数。
宿主支持。 Lithe 桌面端声明
backend和pluginFunctions(版本都是0)。一次会话能调用哪些宿主方法,以运行时lithe/host/getCapabilities返回的services为准,不要按本页假定。 桌面端的Declared()注释列出了已注册的后端宿主服务:工作区上下文(workspace/listContexts、getContext、getFolders和workspace/didChangeContexts)、subscriptions/dispose、languageServer/*、跨插件调用中转、credentials/*、documents/list与getSnapshot、editor/getActive、getSelections、open、reveal、insertText、replaceSelections、applyEdits、window/*、browser/openExternal(只接受 HTTP(S) 地址)、terminal/*和documents/registerContentProvider;与窗口绑定的方法在窗口附着后才注册。 工作区文件与搜索(workspace/readDirectory、stat、readFile、watch、findFiles、findText)、preview/openHTML、editor/registerActions和editor/registerProviders没有注册,申请对应权限的插件会被拒绝。Run 与调试会话方法(run/start、run/stop、debug/start)是否可用,同样以getCapabilities为准。 说明见pluginhost.go中Declared()上方的注释;各能力的对照见能力与协议版本对照。 对契约有疑问,请到 Lithe-core-rust 的 Issues 反馈。
| 项目 | 内容 |
|---|---|
| 清单字段 | backend(与启动进程的字段相同,launch 加 arguments 等);跨插件调用另有 pluginFunctions 段(exports、calls) |
| 能力版本 | backend 0;ui 的后端命令需要同时声明 ui: 0 和 backend: 0;跨插件调用需要 pluginFunctions: 0 和 backend: 0。requires 必须恰好列出清单用到的能力;启动进程是基础契约,不是能力,不写 requires.serverProcess。见版本对照 |
| 协议 | litheFunctions 0(版本就是 backend 的版本)、litheLogging 0,见进程协议版本 |
| API 编号 | API-01、04、05、06、07、08、20、21、22、23、24、35、37、40、41,见 API 总表 |
| 权限 | backend.launch;宿主服务按方法另加权限(credentials.store、workspace.metadata、workspace.files.read、workspace.files.watch、workspace.search、terminal.create、run.start 等),见已公开的宿主方法。跨插件调用没有清单权限:调用方的 calls 和被调方的 exports 就是双方的同意 |
| 激活事件 | 至少要有一种启动方式:网页工具窗口第一次调用 functions.*;ui.commands[] 的命令(onCommand:<id>);可选的 onWorkspaceOpen;或宿主查询的提供者(工具链或快捷键导入提供者,以及 launch.kind: "backend" 的语言服务器)。都没有是 plugin.manifest.invalid。只导出函数的插件不需要网页、命令或 onWorkspaceOpen,被调用时启动后端;只调用的插件仍要有其中一种 |
| 官方 SDK | Node.js 0.5.0、Java 0.5.0、Rust 0.4.0,发布标签 sdk-node-v0.5.0、sdk-java-v0.5.0、sdk-rust-v0.4.0,见官方 SDK。三个 SDK 都实现函数协议,封装凭据、工作区、文档与编辑器(含 editor/registerActions)、预览、浏览器、窗口消息、functions/providers 和 functions/invoke,被调函数能读到 caller,用法见各 SDK 的 README;parentOperationID 不传时,SDK 取正在处理的 lithe/function/invoke 的 operationID。run/*、debug/start、languageServer/*、documents/registerContentProvider 和 editor/registerProviders 没有专门的封装,用 SDK 的通用宿主请求(request)调用。原生终端有封装,见从插件打开原生终端 |
- 每个插件最多一个后端进程,由所有打开的工作区和窗口共享,运行在运行 Lithe 的机器上,远程工作区也一样。工作区不是进程状态:每次调用都显式带工作区上下文,所以
${workspaceStateDir}被拒绝;可写状态是${stateDir},宿主以它为根目录和工作目录启动后端。 - 宿主在上面“激活事件”一行的第一个触发点启动后端;插件被停用、更新、卸载,或宿主退出时结束它。同意来自安装审查,不需要按工作区弹确认;审查里它显示为
packagedBackend、systemCommandBackend或runtimeBackend。 onWorkspaceOpen不会拖慢打开工作区。宿主在窗口显示后一次启动一个这样的后端;在初始化时限内没有完成initialize的后端被停止,按普通的服务器启动失败处理。失败或被停用的后端从不阻塞工作区。workspace.metadata权限授予workspace宿主服务:开放工作区的名称、根目录和 ID、authority 以及插件在该工作区的状态目录;它不读任何文件内容。- 激活只取决于插件已安装、已启用且已审查。Lithe 目前没有工作区信任状态:会运行或修改内容的操作,像宿主自己的控件一样按操作询问用户。
- 插件按平台发布一个包,所以包内路径不随
${arch}变化;macOS 的包可以带通用二进制。
| 项目 | 规则 |
|---|---|
| 协商 | 对插件拥有的会话,宿主在 initialize 的 capabilities.experimental.litheFunctions 提供 {"versions": [0]},与 litheBuild 互不依赖;后端回答 {"version": 0, "functions": [{"id": "…"}]}。回答了宿主没给出的版本,或函数 ID 格式错误、重复,整个协议不启用 |
| 函数 ID | 以点分隔的小驼峰段(snapshot.refresh),不超过 128 字节,会话内唯一,每个会话最多 256 个函数 |
| 注册与撤回 | 初始化之后 lithe/function/register、unregister,整个请求要么全成功要么全失败(-32602、plugin.function.invalidRegistration) |
| 调用 | 宿主发 lithe/function/invoke,顶层有 functionID、operationID、context、params(JSON 对象,默认 {});operationID 由 Core 分配,进度通知必须原样带回 |
| 结果 | 结果,或 plugin.function.failed(后端的消息最多 1024 字节,pluginErrorCode 来自 error.data.code),或 plugin.function.invalidResult(没有 result,或序列化后超过 1 MiB);取消和超时由宿主发 $/cancelRequest |
| 进度 | lithe/function/progress {operationID, message?, percentage?},message 截到 1024 字节,percentage 为 0–100 的整数 |
| 禁止回环 | 后端不能请求宿主分发领域操作或函数:对 lithe/function/invoke 或任何 lithe/build/* 的请求以 plugin.function.recursiveCall 拒绝。跨插件调用是另一种请求,见下文 |
| 私有函数 | 不需要清单字段,增删它们不改变任何版本;只有可被其他插件调用的函数和对它的调用才声明在 pluginFunctions |
| 不回答协议 | 不回答 litheFunctions 的后端(例如只做构建的后端)照常服务它的语言和 litheBuild 请求,收不到函数调用,它的函数请求得到 -32601 |
后端向宿主发请求,前缀 lithe/host/;每个服务由一个清单权限授予,Core 在宿主看到请求之前检查。完整的方法、权限和限制见 API 总表。
| 服务 | 方法 | 权限 |
|---|---|---|
| 能力发现 | getCapabilities、subscriptions/dispose |
每个插件会话都有 |
| 凭据 | credentials/get、set、delete |
credentials.store |
| 工作区上下文 | workspace/listContexts、getContext、getFolders |
workspace.metadata |
| 文件与搜索 | workspace/readDirectory、stat、readFile、watch、findFiles、findText |
workspace.files.read、workspace.files.watch、workspace.search |
| 文档与编辑器 | documents/*、editor/*,见编辑器 |
documents.read、editor.edit、editor.open、editor.actions、editor.providers |
| 预览、浏览器、窗口消息 | preview/openHTML、browser/openExternal、window/* |
preview.open、browser.open、window.messages |
| 原生终端 | terminal/* |
terminal.create |
| Run 与调试会话 | run/start、run/stop、debug/start |
run.start、debug.start |
| 语言服务器 | languageServer/request、notify、setStatus |
languageServer.request |
| 内容提供者 | documents/registerContentProvider |
documents.contentProvider |
| 跨插件调用 | functions/providers、functions/invoke |
无清单权限,见下文 |
- 每个会话最多 16 个未回答的宿主请求;超出的请求以
plugin.host.failed拒绝。Core 按固定顺序拒绝:协议没有定义的方法(-32601、plugin.host.unavailable)、没有授予的服务(-32010、plugin.host.permissionDenied)、参数格式错误(-32602、plugin.host.invalidParams),错误码在error.data.code。宿主自己的失败是-32603、plugin.host.failed。 - 新增宿主服务、方法、通知、可选的请求字段、结果或
limits里的字段、失败码,都不需要新版本;只有现有后端会误读的改动才给backend版本1,协议随之变化。 - 为了在每个宿主上都能工作,后端要遵守四条:只调用
getCapabilities.services列出的方法(其他返回-32601、plugin.host.unavailable);忽略不认识的结果、通知、details、limits字段和不认识的lithe/host/*通知;Core 对方法没有定义的字段返回-32018、plugin.host.unsupportedOption,details.option指明字段名,后端可以去掉该选项重试;把不认识的 JSON-RPC 号当作宿主失败,认识error.data.code时按它分支。 - 后端需要新的服务就声明新的权限;不认识这个权限的宿主在安装时拒绝这个包(
plugin.permission.unknown),权限不会被悄悄授予或忽略。 - 宿主通知(后端不回复):
workspace/didChangeContexts、subscriptions/notify、commands/didRun、run/didExit、languageServer/didChangeState、languageServer/didReceiveNotification,授予条件见 API 总表。后端命令(不打开工具窗口的ui命令)是事件:运行中的后端按顺序收到commands/didRun,已停止的后端只有声明了onCommand:<commandID>才会为它启动。
插件函数默认只属于插件自己。插件导出函数后,另一个已启用、且在清单里声明了这次调用的插件,可以经 Rust Core 调用它:调用方的后端碰不到被调方的进程,被调方的后端也只知道自己正在服务的那次调用;宿主只在被调方的后端没有运行时负责启动它。中转是 Core 的逻辑,在所有运行插件后端的宿主上一样。
| 项目 | 规则 |
|---|---|
| 前提 | 有 backend,requires 写 pluginFunctions: 0 和 backend: 0;requires 列出 pluginFunctions 当且仅当清单有 pluginFunctions 段 |
| 段内容 | exports 与 calls 至少有一个,各最多 32 项、按 id 排序、id 不重复;多余的字段是 plugin.manifest.invalid |
| 导出 | {id, version, context}。id 是函数 ID;version 是导出的契约版本(1–1000 的整数),参数或结果有不兼容变更就换新版本;context 目前只有 "workspace",表示只在本地工作区上下文里运行,缺失或未知则整个清单被拒绝 |
| 调用 | {id, version}。只写函数 ID 和调用方对照的版本,不写被调方的插件 ID,运行时再发现提供者;两个版本必须相等,否则 plugin.function.versionMismatch |
| 注册 | 声明导出不等于注册:被调方的后端仍要 lithe/function/register,未注册或已撤回的函数是 plugin.function.notExported。清单没有导出的函数保持私有,引用它仍是 plugin.function.wrongPlugin |
| 宿主支持 | 宿主向声明了 calls 的插件开放 functions 宿主服务;hostCapabilities 里没有 pluginFunctions 的宿主拒绝这类清单(plugin.api.unsupported) |
只对声明了至少一个 calls 的后端开放,没有声明任何 call 的插件返回 -32010、plugin.host.permissionDenied。getCapabilities 在 functions 服务下列出这两个方法。
方法(前缀 lithe/host/) |
参数 | 结果 |
|---|---|---|
functions/providers |
{functionID} |
{providers: [{pluginID, version}]} |
functions/invoke |
{pluginID, functionID, context: {contextID, generation}, params?, timeoutMilliseconds?, parentOperationID?} |
{value} |
providers列出导出该函数、已启用、且不是调用方自己的插件,按插件 ID 排序,每项带导出的version。调用方必须声明过该函数,否则plugin.function.notDeclared。被调方的后端不必正在运行。invoke调用一个提供者。context必填,是调用方从工作区服务得到的本地工作区上下文;params是 JSON 对象,默认{};timeoutMilliseconds是正整数,默认 60000,上限 600000;未定义的字段返回-32018、plugin.host.unsupportedOption。value是被调方的结果,null也是结果。- 调用方用
$/cancelRequest取消,得到plugin.host.cancelled。 - Core 按固定顺序检查(调用方声明、被调方启用、被调方导出、版本相等、上下文、本地工作区、参数大小、所属调用、调用链、深度、并发数),在向被调方写任何内容之前拒绝,第一个失败的检查决定错误码。
Core 把中转的调用以 lithe/function/invoke 转给被调方。和任何函数调用一样,请求顶层有 functionID、operationID、context、params;中转的调用另有顶层字段 caller: {pluginID}。
caller.pluginID由 Core 按调用会话的身份写入,在params之外,调用方无法提供:functions/invoke请求带顶层caller会被-32018拒绝。要按调用方区分行为或保存状态的函数,读这个字段,不要读params里的插件 ID。调用方params里叫caller的键只是普通数据,原样传递。- 宿主自己发起的调用没有
caller,缺失表示宿主。 operationID标识正在服务的这次调用。被调方在服务一次中转的调用时再发起functions/invoke(嵌套调用),要把这个operationID作为parentOperationID传入,Core 才知道新调用属于哪一次。parentOperationID是 1–128 字节的字符串,否则-32602、plugin.host.invalidParams;不传时,Core 把新调用当作后端正在服务的每一次中转调用的子调用,所以后端应当总是传。- 调用链:新调用的链是父调用的链加上调用方。调用一个已在链上的插件(包括调用自己)是
plugin.function.callCycle,链最长 4 次调用。取消、超时或结束父调用,会取消它的子调用,各被调方收到$/cancelRequest。parentOperationID指向宿主直接发起的调用时,新调用开始一条新链;指向后端没有在服务的调用(已结束或不存在)时被拒绝,plugin.host.cancelled。 - 被调方的后端没有运行时,Core 等待,并在调用方会话上发出运行时事件
functionProviderStartRequired,宿主按普通的插件启动流程启动它;30 秒内没有协商并注册好函数,调用以plugin.function.providerUnavailable结束。不能启动后端的宿主忽略这个事件,调用同样结束。 - Core 为每个后端会话用各自的队列写入:一个后端不再读标准输入,只会拖慢发给它的调用和回复,其他插件的中转照常。向后端的写入在请求时限之后仍被阻塞时,Core 把该后端的会话当作传输失败结束(
transportFailed,阶段relayWrite)。
- 被调方用自己的权限、状态目录和凭据运行;调用方不获得被调方的任何权限,被调方也不获得调用方的。调用方的插件 ID 只出现在
caller字段里,不写进被调方的会话。 - 导出的函数不得接受调用方给的可执行路径、命令行或脚本去启动进程,也不得读写调用方指定的路径而不核对自己的访问范围。调用方需要被调方运行什么,函数应当接收一个由被调方自己解释的请求(任务名、文档),由被调方决定可执行文件。
- 这不是安装依赖:安装调用方不会连带安装被调方,停用被调方只会让调用找不到提供者;不参与安装闭包,不做版本求解。远程工作区的上下文被拒绝,
plugin.host.authorityUnsupported;导出的context: "workspace"只指本地工作区,后端仍在运行 Lithe 的机器上。 - 停用、启用、替换和卸载插件对中转立即生效,不需要重启;宿主用 Core 命令
lsp.configureFunctionRelay发送插件和工作区上下文的快照。只在启动时注册后端插件的宿主上,新安装或更新的后端插件要重启 Lithe 后才会出现在providers里。
| 错误码 | 何时 |
|---|---|
plugin.function.notDeclared |
调用方没有声明该函数;调用进行中调用方更新后不再声明,也是它 |
plugin.function.providerDisabled |
被调方没有安装或未启用;调用进行中被调方被停用、卸载,或更新后不再导出该函数或改了版本,也是它 |
plugin.function.notExported |
被调方的后端没有注册该函数,或已撤回 |
plugin.function.versionMismatch |
调用方声明的版本与被调方导出的版本不相等 |
plugin.function.callCycle |
被调方已在调用链上,包括调用自己 |
plugin.function.depthExceeded |
调用链超过 4 次调用 |
plugin.function.tooManyCalls |
调用方已有 8 个中转调用在运行 |
plugin.function.payloadTooLarge |
参数超过 256 KiB,或被调方的结果超过 256 KiB(超过 1 MiB 是 invalidResult) |
plugin.function.providerUnavailable |
被调方的后端 30 秒内没有就绪;或调用期间后端退出、撤回了该函数 |
被调方自己的失败保留原意:函数回答了错误时是 plugin.function.failed(-32070),消息截到 1024 字节,details.pluginErrorCode 是被调方的 error.data.code;没有 result,或结果序列化后超过 1 MiB,是 plugin.function.invalidResult(-32071)。这两个号都不是 -32603,后者仍是 Core 内部失败 plugin.host.failed。工作区上下文被关闭或换代是 plugin.host.contextExpired,截止时间到了是 plugin.host.timeout;被调方在截止时间后 2 秒仍未回答时,中转自己以 plugin.host.timeout 结束调用并取消被调方的操作。Core 不重启后端,也不重试调用。
后端可以启动宿主当作自己的会话呈现的运行或调试:后端决定启动什么,宿主拥有进程、控制台、停止按钮和会话的结束。Run 会话只增加宿主呈现(Run 窗口里的条目、控制台、退出状态),不授予其他能力。run/start 与 debug/start 带 context: {contextID, generation} 和对应权限;run/stop 不带 context,所以启动它的上下文关闭后仍能停止。
方法(前缀 lithe/host/) |
权限 | 参数 | 结果 |
|---|---|---|---|
run/start |
run.start |
context, label, process, temporaryFiles?, debug?, rerun? |
{runID} |
run/stop |
run.start |
{runID} 或 {sessionID} |
null |
debug/start |
debug.start |
context, label, adapter, request, configuration |
{sessionID},见调试适配器 |
通知(后端不回复):lithe/host/run/didExit,参数 {runID, exitCode?, signal?},授予条件是 run.start。run.start 授予 run/start、run/stop 和 run/didExit。
run/start 的参数:
label:1–256 个字符,不含控制字符,作为 Run 窗口条目的标题。process:{executable, arguments, workingDirectory, environment},四个字段都必填,未知字段被拒绝。executable是{kind: "tool", tool}(tool是清单requiresTools列出的工具定义 ID,宿主按工具链的分层解析)或{kind: "packaged", path}(插件包内文件的规范化相对路径)。参数、工作目录(相对工作区根、且是其中已存在的目录)和环境的上限同运行准备。temporaryFiles:最多 4 个文件,每个最多 4 MiB。{name, contents}是文本,宿主按 UTF-8 写入;{name, base64}是标准 base64 字节,宿主原样写入,不转码,用于编码必须由插件决定的格式(例如按系统代码页读取参数文件的启动器)。两者都有或都没有时在temporaryFiles[<n>]被拒绝。宿主在自己的临时目录写入,运行结束时删除,并把process.arguments和process.environment里的${temporaryFile:<name>}替换成文件路径,插件不需要知道临时目录在哪。name唯一,1–64 个字母、数字、.、_、-,以字母或数字开头。debug:{configurationType, target},让这次运行成为调试会话,见调试适配器。configurationType是公开的调试适配器别名;target是{kind: "connection", port}(进程在port,1–65535,监听调试器,由后端写进参数,宿主等待并附加适配器)或{kind: "launch"}(适配器自己启动进程,按targetBindings从process取程序、参数、目录和环境)。rerun:{function, data},插件已注册的函数和它的参数(最多 64 KiB 的 JSON 对象)。用户选“再次运行”时,宿主以data为参数、当前context调用该函数,后端再回答一次新的run/start,所以重启由后端准备;没有rerun的条目不能再次运行。
debug/start 的参数:
label同run/start。adapter是本插件自己的适配器,写本地 ID 或限定的<plugin-id>/<local-id>;其他插件的适配器和本插件没有声明的适配器都是plugin.host.notFound。request是launch或attach,适配器必须支持它。configuration是最多 256 KiB 的 JSON 对象,宿主在适配器声明的默认值之上把它作为 DAP 的 launch 或 attach 参数传给适配器;noDebug和所有以__开头的键归宿主所有,被拒绝。
其他规则:
run/stop按runID停止运行,或按sessionID停止调试会话,已经结束时同样返回null;取消宿主请求等同停止。后端只能停止自己启动的运行和会话。- 进程结束时宿主发
run/didExit,每次运行只发一次,只发给持有run.start的会话;exitCode是退出状态,signal(1–127)是结束它的信号,宿主丢失进程时两个字段都没有。 - 参数格式错误是
plugin.host.invalidParams,Core 的拒绝(plugin.run.invalidStart、plugin.debug.invalidStart)在details.code和details.field;上下文已关闭是plugin.host.contextExpired;tool解析不到可执行文件是plugin.host.toolUnresolved。 - 插件被禁用或后端退出时,宿主保留运行中的进程,只是不再通知后端;重新启动的后端不会收到它没见过开始的运行的
run/didExit。 - 想让 Run 控制台里的输出出现链接,用
outputLinkMatchers声明匹配器:Core 负责匹配,resolve函数在用户点击时才调用,见 Output link matchers。
| 章节 | 内容 |
|---|---|
| Plugin backend | 后端声明、状态目录、启动与停止时机、按平台发布 |
| Backend lifecycle and workspace contexts | 启动方式、onWorkspaceOpen 不阻塞工作区、workspace.metadata |
| Plugin functions | 协议总览 |
| Capability ownership | 插件函数不能代替领域契约 |
| Declarative or backend implementation | 什么时候用声明式数据、什么时候写后端 |
| Negotiation | initialize 里的版本协商 |
| References and IDs | 函数引用和 ID 规则 |
| Registration and withdrawal | 注册和撤回函数 |
| Invocation、Progress | 调用、结果、进度、取消 |
| Host services | 凭据服务 |
| Workspace contexts and capability discovery | getCapabilities、上下文 contextID/generation、宿主失败码 |
| Host notifications | workspace/didChangeContexts、subscriptions/notify、commands/didRun、run/didExit、语言服务器通知 |
| Extending the host services | 不升版本地增加方法,后端要遵守的四条 |
| Public document, file and window methods | 文件读取、监听、搜索与窗口消息(文档和编辑器方法见编辑器) |
| Plugin-owned native terminals、Close, retained context and reopen | 插件自有原生终端 |
| Run and debug sessions | run/start、run/stop、debug/start、run/didExit:参数、限制、失败码 |
| Language server host methods | 后端与本插件自己的语言服务器对话 |
| Content providers | 把 URI scheme 绑定到后端函数 |
| Backend-only commands | 不打开工具窗口的命令 |
| Cross-plugin calls | pluginFunctions 清单声明、functions/providers、functions/invoke、检查顺序、调用链、caller 字段、未运行的提供者、热生效 |
| Output link matchers | Run 与测试控制台里的输出链接,resolve 函数在点击时调用 |
| Structured plugin diagnostics | lithe/log 的字段和限制 |
| Compatibility、Official backend SDK | 兼容规则与官方 SDK |
| Outcomes and error codes | 结果与错误码 |
| 文件 | 内容 |
|---|---|
backend/backend.plugin.json |
网页工具窗口调用后端,使用凭据服务 |
backend/backend-workspace.plugin.json |
onWorkspaceOpen 启动,workspace.metadata |
backend/command-backend.plugin.json |
后端加 ui:不打开工具窗口的命令,读文档并编辑 |
backend/terminal-backend.plugin.json |
插件自有原生终端的最小清单 |
backend/function-protocol.json |
插件函数协议的标准报文:协商、注册、调用、结果、进度、宿主请求(hostRequests)、调用上下文(invocationContexts)、被中转调用的 caller(invocationCallers)、宿主失败码(hostFailures)、中转失败码(relayErrors)和宿主通知(hostNotifications) |
backend/plugin-functions-exporter.plugin.json |
只导出两个函数(document.format 版本 1、symbols.list 版本 2,context 为 workspace),没有网页、命令或 onWorkspaceOpen |
backend/plugin-functions-caller.plugin.json |
声明调用 document.format 版本 1,onWorkspaceOpen 启动 |
backend/plugin-logging.json |
litheLogging:提供与回答、通知、拒绝用例、级别 |
run/run-start.json |
run/start 的参数和 run.decodeStart 对每个用例的返回(工具与包内可执行文件、debug 与 rerun、各种拒绝);临时文件依赖宿主的临时目录,由 Core 自己的测试覆盖 |
run/tool-reference-launch.plugin.json |
run-start.json 使用的清单:requiresTools 列出工具 interpreter,声明 run.start、debug.start,并带 outputLinkMatchers |
run/output-link-matchers.json |
outputLinks.match 的请求与返回的链接,以及匹配器 resolve 函数的回复经 outputLinks.decodeResolution 得到的结果 |
debug-adapters/debug-start.json |
debug.decodeBackendConfiguration 对 debug/start 每个用例的返回,含拒绝 |
后端相关的拒绝用例在 manifest/invalid-manifests.json。
A plugin backend is the plugin's own long-lived process. It runs what a web page cannot: database drivers, network clients, file-format parsers, calls to host services. It serves only its plugin, belongs to no build system, language, or workspace, and never answers domain requests such as lithe/build/* or VCS. Backend and host speak the plugin-function protocol (litheFunctions); the backend may request the host services its permissions grant, may start Run and debug sessions that the host owns, and may call functions that other plugins export, through Core.
Host support. The Lithe desktop declares
backendandpluginFunctions(both version0). Which host methods a session may call is decided byservicesin the run-timelithe/host/getCapabilitiesanswer; do not assume it from this page. The comment aboveDeclared()in the desktop lists the backend host services it registers: workspace contexts (workspace/listContexts,getContext,getFolders, andworkspace/didChangeContexts),subscriptions/dispose,languageServer/*, the cross-plugin call relay,credentials/*,documents/listandgetSnapshot,editor/getActive,getSelections,open,reveal,insertText,replaceSelections,applyEdits,window/*,browser/openExternal(HTTP(S) addresses only),terminal/*, anddocuments/registerContentProvider; methods bound to a window are registered once the window attaches. Workspace files and search (workspace/readDirectory,stat,readFile,watch,findFiles,findText),preview/openHTML,editor/registerActions, andeditor/registerProvidersare not registered, and a plugin that asks for those permissions is refused. Whether the Run and debug session methods (run/start,run/stop,debug/start) are available is likewise decided bygetCapabilities. See the comment aboveDeclared()inpluginhost.go; for a per-capability comparison see the capability and protocol versions. Questions about the contract go to Lithe-core-rust Issues.
| Item | Value |
|---|---|
| Manifest field | backend (the same fields as process start: launch, arguments, and so on); cross-plugin calls add a pluginFunctions section (exports, calls) |
| Capability versions | backend 0; backend commands of ui need both ui: 0 and backend: 0; cross-plugin calls need pluginFunctions: 0 and backend: 0. requires must list exactly the capabilities the manifest uses; process start is base contract, not a capability, so do not write requires.serverProcess. See versions |
| Protocols | litheFunctions 0 (the version is the version of backend), litheLogging 0; see process protocol versions |
| API IDs | API-01, 04, 05, 06, 07, 08, 20, 21, 22, 23, 24, 35, 37, 40, 41; see the API catalog |
| Permissions | backend.launch; host services need their own permission per method (credentials.store, workspace.metadata, workspace.files.read, workspace.files.watch, workspace.search, terminal.create, run.start, …); see public host methods. Cross-plugin calls have no manifest permission: the caller's calls and the provider's exports are the consent of the two sides |
| Activation | At least one way to start is required: a web tool window's first functions.* call; a ui.commands[] command (onCommand:<id>); the optional onWorkspaceOpen; or a provider the host queries (a toolchain or keymap import provider, or a language server with launch.kind: "backend"). None of them is plugin.manifest.invalid. A plugin that only exports functions needs no page, command, or onWorkspaceOpen: a call starts its backend; a plugin that only calls still needs one of them |
| Official SDKs | Node.js 0.5.0, Java 0.5.0, Rust 0.4.0, released as sdk-node-v0.5.0, sdk-java-v0.5.0, and sdk-rust-v0.4.0; see official SDKs. All three implement the function protocol and wrap credentials, workspaces, documents and editors (including editor/registerActions), preview, browser, window messages, functions/providers, and functions/invoke; a called function can read caller; see each SDK's README for usage. When parentOperationID is omitted, the SDK uses the operationID of the lithe/function/invoke being handled. run/*, debug/start, languageServer/*, documents/registerContentProvider, and editor/registerProviders have no dedicated wrappers; call them through the SDK's generic host request (request). Native terminals are wrapped; see Open a native Lithe terminal from a plugin |
- A plugin has at most one backend process, 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 context explicitly, so
${workspaceStateDir}is refused; the writable state is${stateDir}, and the host starts the backend with it as root and working directory. - The host starts the backend on the first trigger in the "Activation" row above, and ends it when the plugin is disabled, updated, or uninstalled, and when the host quits. Consent is the install review, which lists it as
packagedBackend,systemCommandBackend, orruntimeBackend; no per-workspace prompt is needed. - Starting on
onWorkspaceOpennever delays opening the workspace. The host starts such backends after the window is shown, one at a time; 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: 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.
- A plugin publishes one archive per platform, so its packaged path does not vary by
${arch}; a macOS archive may carry a universal binary.
| Item | Rule |
|---|---|
| Negotiation | For a plugin-owned session the host offers {"versions": [0]} as capabilities.experimental.litheFunctions in initialize, independent of litheBuild; the backend answers {"version": 0, "functions": [{"id": "…"}]}. An answer with a version the host did not offer, or a malformed or duplicate function ID, does not negotiate the protocol at all |
| Function ID | Dot-separated lowerCamelCase segments (snapshot.refresh), at most 128 bytes, unique within the session; a session holds at most 256 functions |
| Registration and withdrawal | After initialize, lithe/function/register and unregister; each request is all or nothing (-32602, plugin.function.invalidRegistration) |
| Invocation | The host sends lithe/function/invoke with top-level functionID, operationID, context, and params (a JSON object, default {}); Core assigns operationID, and progress notifications must echo it unchanged |
| Outcome | A result, or plugin.function.failed (the backend's message up to 1024 bytes, with pluginErrorCode from error.data.code), or plugin.function.invalidResult (no result, or over 1 MiB serialized); the host cancels and times out with $/cancelRequest |
| Progress | lithe/function/progress {operationID, message?, percentage?}; message is cut to 1024 bytes and percentage is a whole number from 0 to 100 |
| No loops | A backend never asks the host to dispatch a domain operation or a function: a request for lithe/function/invoke or any lithe/build/* method is refused with plugin.function.recursiveCall. Cross-plugin calls are a different request; see below |
| Private functions | They need no manifest field, and adding or removing them changes no version; only a function another plugin may call, and a call to it, is declared in pluginFunctions |
| Not answering the protocol | A backend that does not answer litheFunctions (a build-only backend, for example) keeps serving its language and its litheBuild requests and receives no function calls; its function requests get -32601 |
A backend sends requests to the host with the prefix lithe/host/. Each service is granted by one manifest permission, and Core checks every request before the host sees it. The full methods, permissions, and limits are in the API catalog.
| Service | Methods | Permission |
|---|---|---|
| Capability discovery | getCapabilities, subscriptions/dispose |
Every plugin session has it |
| Credentials | credentials/get, set, delete |
credentials.store |
| Workspace contexts | workspace/listContexts, getContext, getFolders |
workspace.metadata |
| Files and search | workspace/readDirectory, stat, readFile, watch, findFiles, findText |
workspace.files.read, workspace.files.watch, workspace.search |
| Documents and editors | documents/*, editor/*; see Editor |
documents.read, editor.edit, editor.open, editor.actions, editor.providers |
| Preview, browser, window messages | preview/openHTML, browser/openExternal, window/* |
preview.open, browser.open, window.messages |
| Native terminals | terminal/* |
terminal.create |
| Run and debug sessions | run/start, run/stop, debug/start |
run.start, debug.start |
| Language servers | languageServer/request, notify, setStatus |
languageServer.request |
| Content providers | documents/registerContentProvider |
documents.contentProvider |
| Cross-plugin calls | functions/providers, functions/invoke |
No manifest permission; see below |
- A session holds at most 16 unanswered host requests; a further one is refused with
plugin.host.failed. Core refuses 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); the code is inerror.data.code. A failure of the host itself is-32603,plugin.host.failed. - A later host adds a service, a method, a notification, an optional request field, a field of a result or of
limits, or a failure code without a new version; only a change an existing backend would misread givesbackendversion1, and the protocol moves with it. - To work with every host, a backend follows four rules: 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.optionnames it, so the backend may retry without that option; and it treats a JSON-RPC number it does not know as a host failure, branching onerror.data.codewhen it knows that code. - A backend that 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. - Host notifications (the backend does not reply):
workspace/didChangeContexts,subscriptions/notify,commands/didRun,run/didExit,languageServer/didChangeState, andlanguageServer/didReceiveNotification; the grants are in the API catalog. Backend commands (auicommand that opens no tool window) are events: a running backend receivescommands/didRunin order, and a stopped one is started for it only when it declaresonCommand:<commandID>.
A plugin function is private to its plugin unless the plugin exports it. An exported function can be called, through Rust Core, by another enabled plugin whose manifest declares that 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. The relay is Core logic, the same on every host that runs plugin backends.
| Item | Rule |
|---|---|
| Prerequisites | A backend, and requires with pluginFunctions: 0 and backend: 0; requires lists pluginFunctions exactly when the manifest has a pluginFunctions section |
| Section | At least one of exports and calls, each at most 32 entries, sorted by id, no id twice; any other field is plugin.manifest.invalid |
| Export | {id, version, context}. id is a function ID; version is the exported contract version (an integer from 1 to 1000), and an incompatible change to the params or result takes a new version; context has one value for now, "workspace", meaning the function runs only for a local workspace context; a missing or unknown context refuses the whole manifest |
| Call | {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; the two versions must be equal, otherwise plugin.function.versionMismatch |
| Registration | Declaring an export does not register it: the provider's backend still calls lithe/function/register, and a function it has not registered, or has withdrawn, is plugin.function.notExported. A function the manifest does not export stays private, and a reference to it is still plugin.function.wrongPlugin |
| Host support | The host grants a plugin that declares calls the functions host service; a host without pluginFunctions in hostCapabilities refuses such a manifest (plugin.api.unsupported) |
Only a backend whose plugin declares at least one call may use them; a plugin that declares no call gets -32010, plugin.host.permissionDenied. getCapabilities lists both methods under the functions service.
Method (prefix lithe/host/) |
Params | Result |
|---|---|---|
functions/providers |
{functionID} |
{providers: [{pluginID, version}]} |
functions/invoke |
{pluginID, functionID, context: {contextID, generation}, params?, timeoutMilliseconds?, parentOperationID?} |
{value} |
providerslists the enabled plugins other than the caller that export the function, sorted by plugin ID, each with its exportedversion. The caller must have declared the function, otherwiseplugin.function.notDeclared. A provider's backend need not be running.invokecalls one provider.contextis required: a local workspace context the caller learned from the workspace service.paramsis a JSON object and defaults to{}.timeoutMillisecondsis a positive integer, defaults to 60000, and is capped at 600000. A field the method does not define is-32018,plugin.host.unsupportedOption.valueis the provider's result, andnullis a result.- The caller cancels a call with
$/cancelRequest; it fails withplugin.host.cancelled. - Core checks in a fixed order (the caller declared the function, the provider is enabled, the provider exports it, the versions are equal, the context, a local workspace, the params size, the call it belongs to, the call chain, the depth, the concurrency) and refuses before anything is written to the provider; the first failing check decides the code.
Core forwards a relayed call to the provider as lithe/function/invoke. As for any function call, the request has top-level functionID, operationID, context, and params; a relayed call adds one more top-level field, caller: {pluginID}.
- Core writes
caller.pluginIDfrom the identity of the calling session. It sits outsideparamsand the caller cannot supply it: afunctions/invokerequest carrying a top-levelcalleris refused with-32018. A function that decides by who asks, or keeps per-caller state, reads this field, never a plugin ID inparams. A key namedcallerinside the caller's ownparamsis ordinary data and is passed unchanged. - A call the host itself started has no
caller; absent means the host. operationIDidentifies the call being served. A provider that makes a nestedfunctions/invokewhile serving a relayed call setsparentOperationIDto thisoperationID, so Core knows which call the new one belongs to.parentOperationIDis a string of 1 to 128 bytes, otherwise-32602,plugin.host.invalidParams. Without it, Core treats the new call as a child of every relayed call the backend is serving, so a backend should always set it.- The call chain: the chain of a new call is the chain of its parent plus the caller. A call to a plugin already on the chain, including a plugin calling itself, is
plugin.function.callCycle, and a chain has at most 4 calls. Cancelling, timing out, or ending the parent call cancels its children, and each provider receives$/cancelRequest. IfparentOperationIDnames a call the host started directly, the new call starts a new chain; if it names a call this backend is not serving (finished, or never existed), the call is refused withplugin.host.cancelled. - If the provider's backend is not running, Core waits for it and emits the runtime event
functionProviderStartRequiredon the calling session; the host starts the backend as an ordinary plugin start. If the provider has not negotiated the protocol and registered the function within 30 seconds, the call ends withplugin.function.providerUnavailable. A host that cannot start the backend ignores the event, and the call ends the same way. - 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, and 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, stagerelayWrite).
- The provider runs with its own permissions, state directory, and credentials. The caller gains none of the provider's, and the provider gains none of the caller's. The caller's plugin ID appears only in the
callerfield, never in the provider's session. - 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.
- This is not an install dependency: installing the caller does not install a provider, and disabling a provider only leaves calls without one; it takes no part in the install closure and does no version solving. A call whose context is a remote workspace is refused with
plugin.host.authorityUnsupported; the exportedcontext: "workspace"means a local workspace only, and backends still run on the machine that runs Lithe. - Disabling, enabling, replacing, and uninstalling a plugin take effect for the relay at once, with no restart; the host sends the snapshot of plugins and workspace contexts with the Core command
lsp.configureFunctionRelay. On a host that registers backend plugins only when it launches, a newly installed or updated backend plugin appears inprovidersonly after Lithe restarts.
| Code | When |
|---|---|
plugin.function.notDeclared |
The caller did not declare the function; also when a caller update stops declaring it while the call runs |
plugin.function.providerDisabled |
The provider is not installed or not enabled; also when, while the call runs, the provider is disabled or uninstalled, or its update stops exporting the function or changes its version |
plugin.function.notExported |
The provider's backend has not registered the function, or has withdrawn it |
plugin.function.versionMismatch |
The version the caller declared differs from the version the provider exports |
plugin.function.callCycle |
The provider is already on the call chain, including the caller itself |
plugin.function.depthExceeded |
The chain would have more than 4 calls |
plugin.function.tooManyCalls |
The caller already has 8 relayed calls running |
plugin.function.payloadTooLarge |
The params are over 256 KiB, or the provider's result is over 256 KiB (over 1 MiB is invalidResult) |
plugin.function.providerUnavailable |
The provider's backend was not ready within 30 seconds, or it exited or withdrew the function while the call ran |
A provider's own failure keeps its meaning: when its function answered with an error, the code is plugin.function.failed (-32070), the message is cut to 1024 bytes, and details.pluginErrorCode is the provider's own error.data.code; a reply with no result, or a result over 1 MiB serialized, is plugin.function.invalidResult (-32071). Neither number is -32603, which stays plugin.host.failed, a failure inside Core. A workspace context that is closed or replaced by a newer generation is plugin.host.contextExpired, and the deadline passing is plugin.host.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. Core never restarts a backend or retries a call.
A backend can start a run or a debug session that the host presents as its own: the backend decides what to start, and 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 console, the exit status) and 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}; see Debug adapters |
Notification (the backend does not reply): lithe/host/run/didExit with {runID, exitCode?, signal?}, granted by run.start. run.start grants run/start, run/stop, and run/didExit.
run/start parameters:
label: 1 to 256 characters without control characters; it titles the Run window entry.process:{executable, arguments, workingDirectory, environment}, all four required; an unknown field is refused.executableis{kind: "tool", tool}(toolis a tool-definition ID that the manifest lists inrequiresTools, which the host resolves through the layers described in Toolchains) 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.temporaryFiles: at most four files of up to 4 MiB each.{name, contents}is text the host writes as UTF-8;{name, base64}is bytes in standard base64 the host writes unchanged, without transcoding, for a format whose encoding the plugin must choose (such as a launcher that reads its argument file in the system code page). A file with both or neither is refused attemporaryFiles[<n>]. The host writes them into its own temporary directory, removes them when the run ends, and replaces${temporaryFile:<name>}inprocess.argumentsandprocess.environmentwith the file's path, so the plugin never needs to know where the host keeps temporary files. Anameis unique, 1 to 64 letters, digits,.,_, or-, and starts with a letter or digit.debug:{configurationType, target}, which makes the run a debug session; see Debug adapters.configurationTypeis a public debug adapter alias;targetis{kind: "connection", port}(the process listens for a debugger onport, 1 to 65535, which the backend puts 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).rerun:{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.
debug/start parameters:
labelis as inrun/start.adapteris one of the plugin's own adapters, by its local ID or its qualified<plugin-id>/<local-id>; an adapter of another plugin, and one the plugin does not declare, are bothplugin.host.notFound.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.
Other rules:
run/stopstops a run byrunIDor a debug session bysessionIDand answersnull, also when it already ended; cancelling the host request is the same as stopping. A backend can stop only runs and sessions it started.- The host sends
run/didExitwhen the process ends, once per run and only to a session that holds therun.startgrant.exitCodeis the exit status andsignal(1 to 127) the signal that ended it; both are absent when the host lost the process. - Malformed params are
plugin.host.invalidParams, and Core's rejection (plugin.run.invalidStartorplugin.debug.invalidStart) is indetails.codeanddetails.field; a closed context isplugin.host.contextExpired; atoolthat resolves to no executable isplugin.host.toolUnresolved. - The host keeps running processes when the plugin is disabled or its backend exits; it only stops telling the backend. A backend that starts again does not receive
run/didExitfor a run it did not see begin. - To make output in the Run console clickable, declare matchers with
outputLinkMatchers: Core does the matching and theresolvefunction is called only when the user clicks; see Output link matchers.
| Section | Content |
|---|---|
| Plugin backend | Declaration, state directory, when it starts and stops, per-platform archives |
| Backend lifecycle and workspace contexts | Start routes, onWorkspaceOpen never blocks the workspace, workspace.metadata |
| Plugin functions | Protocol overview |
| Capability ownership | Plugin functions never replace a domain contract |
| Declarative or backend implementation | When to use declarative data and when to write a backend |
| Negotiation | Version negotiation in initialize |
| References and IDs | Function references and ID rules |
| Registration and withdrawal | Registering and withdrawing functions |
| Invocation, Progress | Calls, results, progress, cancellation |
| Host services | Credentials service |
| Workspace contexts and capability discovery | getCapabilities, contextID/generation contexts, host failure codes |
| Host notifications | workspace/didChangeContexts, subscriptions/notify, commands/didRun, run/didExit, language server notifications |
| Extending the host services | How services gain methods without a new version, and the four rules for a backend |
| Public document, file and window methods | File reads, watches, search, and window messages (document and editor methods are on Editor) |
| Plugin-owned native terminals, Close, retained context and reopen | Plugin-owned native terminals |
| Run and debug sessions | run/start, run/stop, debug/start, run/didExit: params, limits, failure codes |
| Language server host methods | A backend talking to its own plugin's language servers |
| Content providers | Binding a URI scheme to a backend function |
| Backend-only commands | Commands that open no tool window |
| Cross-plugin calls | The pluginFunctions manifest declarations, functions/providers, functions/invoke, the check order, the call chain, the caller field, providers that are not running, hot activation |
| Output link matchers | Links in Run and Test console output; the resolve function is called on click |
| Structured plugin diagnostics | lithe/log fields and limits |
| Compatibility, Official backend SDK | Compatibility rules and the official SDKs |
| Outcomes and error codes | Outcomes and error codes |
| File | Content |
|---|---|
backend/backend.plugin.json |
A web tool window calls the backend, which uses the credentials service |
backend/backend-workspace.plugin.json |
Starts on onWorkspaceOpen, uses workspace.metadata |
backend/command-backend.plugin.json |
A backend with ui: a command without a tool window that reads and edits documents |
backend/terminal-backend.plugin.json |
The smallest manifest for plugin-owned native terminals |
backend/function-protocol.json |
Canonical plugin-function messages: negotiation, registration, invocation, results, progress, 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) |
backend/plugin-functions-exporter.plugin.json |
Only exports two functions (document.format version 1, symbols.list version 2, context workspace), with no page, command, or onWorkspaceOpen |
backend/plugin-functions-caller.plugin.json |
Declares a call to document.format version 1, starts on onWorkspaceOpen |
backend/plugin-logging.json |
litheLogging: offer and answer, notifications, rejections, levels |
run/run-start.json |
The params of run/start and what run.decodeStart returns for each case (tool and packaged executables, debug and rerun, the refusals); temporary files depend on the host's temporary directory and are covered by Core's own tests |
run/tool-reference-launch.plugin.json |
The manifest run-start.json uses: requiresTools lists the tool interpreter, and it declares run.start, debug.start, and outputLinkMatchers |
run/output-link-matchers.json |
The requests of outputLinks.match and the links it returns, plus what outputLinks.decodeResolution makes of a matcher's resolve reply |
debug-adapters/debug-start.json |
What debug.decodeBackendConfiguration returns for each debug/start case, including the refusals |
Backend rejections are in manifest/invalid-manifests.json.