docs(plugin): refocus the certification page on developer decisions

Rewrite plugin/certified-plugins in all three locales around the
questions a plugin developer actually asks, and make it the single home
for certification: what it is, why it exists, when it is needed, what
changes after approval, and what the contract requires.

- Decision framing restored, including a needs/does-not-need table and
  the explicit statement that most plugins do not need certification.
- Full manifest example, accepted values for both declaration fields and
  the SDK version that recognises them.
- Constraints kept and tightened: stateless rules, allowed/forbidden
  table, anti-patterns with correct/incorrect snippets, Runner notes,
  self-check list.
- Submission flow moved onto this page so it no longer depends on the
  publishing page.
- Operator content that duplicates the Core architecture document
  removed (Cloud admission matrix, OSS issuer keys, certificate fields,
  internal review tiers).

publish/market is now publishing-only with a one-line pointer, and its
misplaced plugin_submission screenshot moves back under the upload
section where it belongs. dev/basic-info replaces the commented-out
manifest fields with a link to the contract page.

ja had been missing whole sections (36 lines); all three locales now
share the same section structure.
This commit is contained in:
Hyu
2026-09-29 14:40:58 +08:00
parent aab1e66ac1
commit 85fb49c687
9 changed files with 499 additions and 107 deletions
+157 -28
View File
@@ -1,60 +1,189 @@
---
title: "认证插件与无状态组件"
description: "认证插件为什么共享 Worker,以及 stateless-v1 组件规范。"
title: "认证插件与共享运行"
description: "认证是什么、为什么存在、什么时候需要,以及 shared-runtime-v1 / stateless-v1 的适配规范。"
---
认证只代表精确、不可变的插件制品可在 LangBot Cloud 跨租户共享,不是一般的质量或安全徽章。安装的制品拥有有效可信签名后必须以 shared 模式运行,不得回退到 dedicated。市场上的**认证插件**标识本身不能证明下载的 ZIP 已通过验证,也不能证明线上 Worker 实际共享。相同 artifact digest 的安装共享一个 Worker、一个 `BasePlugin` 实例和一组组件实例;每个安装只保留轻量的配置、权限与生命周期上下文。这里的“一组组件”指每个已声明组件 manifest 各一个实例,包括 EventListener、Tool、Command、KnowledgeEngine、Parser、Page 和 Runner。
## 认证是什么
## 声明
认证是 LangBot 对**一份精确、不可变的插件制品**授予的资格:该制品被批准在 LangBot Cloud 中**跨租户共享运行**。
- 认证绑定到制品本身(已发布的具体版本,按 digest 区分),不是绑定到插件名或作者。同一个插件发布新版本后,上一版的认证不会自动延续。
- 认证不是质量评分,也不是安全审计背书。
## 为什么需要认证
平台默认**不**让插件共享运行。这是因为共享的粒度是制品:
- **专属模式**:插件不参与共享,独立运行。
- **共享模式**:同一个 artifact digest 的所有安装共用一个 Worker。
共享的好处是进程数量和启动开销不随安装数线性增长。代价是**一个进程里会同时处理多个工作区的调用**——如果插件把租户数据放在实例上,就会在租户之间串数据。
所以平台需要先对这份制品做代码审查、确认它满足无状态约束,再用签名把"对这份制品的信任"固定下来,才敢让它进入共享放置。这就是认证存在的原因。
## 什么情况下需要认证
**大多数插件不需要认证。** 不认证不影响你把插件发布到插件市场,也不影响用户安装使用——插件会以专属模式正常运行。
| 你的情况 | 是否需要认证 |
| --- | --- |
| 只想把插件发布到插件市场供用户使用 | 不需要 |
| 插件需要在进程内保留跨调用状态(例如常驻的外部会话进程) | 不需要,也应该保持专属运行 |
| 插件里有后台任务、定时器,或长期缓存租户数据 | 不需要,保持专属模式更安全 |
| 希望获得认证插件标识 | 需要 |
| 希望同一制品的多个安装共用一个进程,降低资源与启动开销 | 需要 |
| 插件面向 Cloud 多租户场景,希望被平台以共享方式放置 | 需要 |
判断标准只有一条:**只有当你的代码确实能做到"实例上不保存任何租户或安装相关数据"时,认证才有意义。**
## 认证之后的效果
通过审核后:
- 制品被签入证书,安装时以**共享模式**运行;同一 digest 的多个安装共用一个 Worker。
- 该 Worker 中只有一个 `BasePlugin` 实例,以及每个已声明组件 manifest 各一个实例(EventListener、Tool、Command、KnowledgeEngine、Parser、Page、Runner)。
- 每个安装只额外保留一份轻量的配置、权限与生命周期上下文,调用时由当前请求决定使用哪一份。
- 你会获得认证插件标识。
- 自托管 OSS 默认不配置认证签发方公钥,因此即使是带认证的制品,在自托管实例上仍以专属模式运行。
同时你也承担一项长期义务:**后续每个版本都必须继续满足无状态约束**。修改代码后需要提交新版本重新审核,认证不会自动延续。
## 需要做的适配与规范
### 声明
在 `manifest.yaml` 的 `execution` 下同时声明两个字段:
```yaml
apiVersion: v1
kind: Plugin
metadata:
author: your-name
name: YourPlugin
version: 0.1.1
spec:
config: []
components: {}
execution:
python:
path: main.py
attr: Plugin
attr: YourPlugin
sharedRuntime: shared-runtime-v1
componentModel: stateless-v1
```
两个字段必须同时出现。声明只是申请,不能代替绑定精确制品的可信辅助审查、人工批准和有效签名。辅助审查只是内部前置证据,不是中间认证等级。
| 字段 | 合法值 | 说明 |
| --- | --- | --- |
| `execution.sharedRuntime` | `shared-runtime-v1` | 只接受这一个值,填写其他值会导致清单校验失败 |
| `execution.componentModel` | `stateless-v1` | 只接受这一个值 |
## 无状态组件规范
- 两个字段**必须同时声明**。只写 `sharedRuntime` 不写 `componentModel` 的包会在认证校验阶段被拒绝。
- 这两个字段自 **SDK 0.7.0** 起被识别;更早的 SDK 会忽略它们,因此不要用旧版本验证声明是否生效。
- `BasePlugin.initialize()` 和组件 `initialize()` 是进程级初始化,每个 Worker 只运行一次。
- 所有组件实例都可能同时处理不同工作区的调用,必须可重入。
- 在 `BasePlugin` 中使用 `self.get_config()`;在组件中使用 `self.get_plugin_config()` 或 `self.plugin.get_config()` 读取当前安装配置。
- 不得在实例字段或模块全局变量中保存配置、凭据、Workspace/installation ID 或请求数据。
- 不得在 `initialize()` 中读取并保存租户配置。
- 存储、文件、凭据、日志、模型及其他平台能力必须通过 SDK Host API 访问;SDK 从当前 task-local invocation context 取得权限。
- 持久租户状态必须保存在 Host 服务中。进程内 cache 只能保存 artifact 级公共数据,或以完整 installation binding 为 key 并支持撤销清理。
- `stateless-v1` 当前禁止携带租户数据的 detached 后台任务或线程;SDK 尚未提供插件侧所有权/撤销 API。
<Warning>
声明只是申请,不等于通过认证。它本身不会授予认证标识,也不会改变运行模式。
</Warning>
这些约束为未来 serverless 组件执行保留接口语义;当前版本仍运行长生命周期 Worker,并未提供 serverless 部署模式。
### 无状态组件规范
## 配置访问示例
- `BasePlugin.initialize()` 和组件 `initialize()` 是**进程级**初始化,每个 Worker 只执行一次。此时不存在"当前安装",也不存在"当前工作区"。
- 组件实例会被不同工作区并发调用,因此所有方法都必须可重入。
- 读取当前安装的配置:`BasePlugin` 用 `self.get_config()`,组件用 `self.get_plugin_config()` 或 `self.plugin.get_config()`,Runner 可以直接用 `ctx.config`。
- 不得把配置、凭据、工作区或安装标识、请求数据保存在实例字段或模块级全局变量中。
- 不得在 `initialize()` 中读取并缓存安装配置。
- 存储、文件、凭据、日志、模型等平台能力必须通过 SDK 的 Host API 访问;权限由当前调用自动解析,不需要也不应该自己缓存。
- 需要跨调用保留的租户数据必须存放在 Host 服务中。Runner 使用 `ctx.state_get()` / `ctx.state_set()` 等接口(需要 `ctx.context.available_apis.state` 为 `true`)。进程内缓存只允许存放制品级公共数据;如果确实要缓存租户数据,键必须包含完整安装标识,并能在安装被移除时清理。
- 禁止发起携带租户数据的后台任务或线程。处理函数返回后,本次调用的上下文和运行关联调用即失效。
### 允许 / 禁止对照
| 场景 | 允许 | 禁止 |
| --- | --- | --- |
| 读取配置 | 每次调用时读取(`self.get_plugin_config()`、`ctx.config`) | 在 `initialize()` 中读取后保存到 `self` |
| 连接池 / HTTP 客户端 | 不含租户凭据的共享连接池 | 把安装的 endpoint、api key 固化到实例上 |
| 缓存 | 制品级公共数据;或键为完整安装标识且支持清理 | 模块级 `dict` 以工作区或用户为键长期驻留 |
| 跨调用状态 | Host 服务(Runner 用 `ctx.state_*`) | 实例字段、模块级全局变量 |
| 后台工作 | 无 | 处理函数返回后继续持有本次上下文或其关联调用 |
| 日志 | `ctx.log()` 或 SDK 日志 API | 模块级后台线程直接输出租户配置或凭据 |
### 常见反例
**1. 在初始化时读取安装配置**
```python
async def initialize(self):
self.endpoint = self.plugin.config["endpoint"] # 进程级初始化,没有当前安装
self.api_key = self.plugin.config["api_key"]
```
共享初始化阶段没有安装配置,这段代码只会读到空值或上一次的值。
**2. 把租户数据放进模块级缓存**
```python
_CACHE = {} # 以工作区或用户为键,会跨安装、跨租户长期驻留
async def call(self, params, **kwargs):
if params["user"] not in _CACHE:
_CACHE[params["user"]] = await fetch(params["user"])
```
**3. 处理函数返回后继续使用本次调用**
```python
async def call(self, params, **kwargs):
config = self.get_plugin_config()
asyncio.create_task(self._upload_later(config)) # 处理函数已返回,上下文与授权均已失效
return ok()
```
**4. 把实例字段当作配置容器**
```python
async def initialize(self):
self.default_endpoint = "https://api.example.com" # 公共常量没有问题
self.last_config = {} # 一旦写入安装配置就违规
```
正确写法:
```python
class LookupTool(Tool):
async def initialize(self):
# 只放置制品级、不含租户信息的对象
self.transport = StatelessHTTPTransport()
async def call(self, params, **kwargs):
config = self.get_plugin_config()
config = self.get_plugin_config() # 每次调用按当前安装读取
return await self.transport.lookup(config["endpoint"], params)
```
不要这样写:
### Runner 组件特别注意
```python
async def initialize(self):
self.endpoint = self.plugin.config["endpoint"]
self.api_key = self.plugin.config["api_key"]
```
Runner 与无状态约束的张力最大,因为运行器天然倾向于在进程内保存会话与上下文。
共享初始化没有租户配置,因此这段代码会读取空值/旧值或失败;任何把调用配置保存到共享对象的做法都是认证 blocker。
- `RunnerContext` **只在本次运行期间有效**,不要把它或它的字段保存到实例上跨调用复用。
- Runner 需要跨调用保留的会话状态,走 `ctx.state_get()` / `ctx.state_set()`(作用域支持 `conversation`、`actor`、`subject`、`runner`)或 Host 提供的其他存储,不要使用实例字段。
- 处理函数返回后,不要继续在后台任务中使用 `ctx`,也不要继续使用通过 `self.plugin` 发起的运行关联调用。
- 如果 Runner 的设计必须依赖进程内持有的跨调用状态(例如需要长期驻留的外部会话进程),**不要声明 `stateless-v1`**,继续以专属 Worker 运行即可。
## 兼容旧插件
运行上下文的完整接口见[运行上下文(RunnerContext)](/zh/plugin/dev/apis/agent-run)。
旧源码和未签名插件包仍兼容专属 Worker,Cloud 也允许未签名专属安装。不存在专属运行认证或专属签名。Cloud 必须拒绝畸形、伪造、不可信或缺少 `stateless-v1` 声明的旧版签名证书,不能将其当作未签名制品。自托管 OSS 不内置认证签发方公钥,仅支持单工作区;不支持配置公钥获得认证共享运行。已有安装或市场标识不代表获得共享资格。仅修改 manifest 不会通过认证审查。
## 自检与提交
认证流程仍包括辅助代码审查和人工批准,并将插件身份、版本、digest、`shared-runtime-v1` 与 `stateless-v1` 一起签入证书。提交方式参阅[发布到插件市场](/zh/plugin/dev/publish/market)。
提交审核前逐条核对:
- [ ] `manifest.yaml` 中两个字段都已声明,取值正确。
- [ ] 全文搜索,确认没有模块级可变全局变量。
- [ ] 确认 `initialize()` 只做进程级初始化,不读取安装配置。
- [ ] 确认所有安装配置都在调用时读取。
- [ ] 确认没有在处理函数返回后继续运行的任务或线程。
- [ ] 确认跨调用状态通过 Host API 保存(Runner 使用 `ctx.state_*`)。
### 提交方式
认证与发布走同一条链路,不需要额外流程:在 `manifest.yaml` 中声明上面两个字段,然后照常用 `lbp publish` 提交一个**新版本**(已发布的版本无法追加声明)。LangBot Space 会检查本次提交的精确插件包,进行辅助代码审查,并在人工批准后签发认证插件标识;认证绑定到已审核的版本与校验和。
如果审核发现该版本无法满足要求,请先把它作为未签名的普通插件发布,并在后续不可变版本中修复问题。
如果核对下来发现插件并不适合共享运行,直接以普通插件提交即可,不需要认证。发布流程见[发布到插件市场](/zh/plugin/dev/publish/market)。
+3 -4
View File
@@ -28,12 +28,11 @@ execution:
python:
path: main.py # 请勿修改
attr: HelloPlugin # 请勿修改
# 仅在申请认证且代码已迁移为无状态单例时添加:
# sharedRuntime: shared-runtime-v1
# componentModel: stateless-v1
```
插件多语言遵循 [RFC 4646](https://datatracker.ietf.org/doc/html/rfc4646) 标准,目前支持的语言有:
如果需要申请认证并让多个安装共享运行,还需要在 `execution` 下声明 `sharedRuntime` 与 `componentModel`,见[认证插件与共享运行](/zh/plugin/certified-plugins)。
插件多语言遵循 [RFC 4646](https://datatracker.ietf.org/doc/html/rfc4646) 标准。目前支持的语言有:
- `en_US` 英文 (必填)
- `zh_Hans` 简体中文
+2 -8
View File
@@ -78,14 +78,8 @@ Plugin built successfully: dist/tmp/RockChinQ-HelloPlugin-0.1.0.lbpkg
您现在可以在[LangBot 插件市场](https://space.langbot.app/market)查看您的插件提交任务。请等待审核通过后,您可以在插件市场查看您的插件。
## 申请认证插件审核
如果该版本准备申请认证,请在 `manifest.yaml` 中同时声明 `execution.sharedRuntime: shared-runtime-v1` 和 `execution.componentModel: stateless-v1`,并提交一个**新版本**。辅助审查会确认单例组件无状态、可重入、按调用读取配置且不持有租户数据。声明本身不会授予认证标识或共享运行权限。
LangBot Space 会检查本次提交的精确插件包,进行辅助代码审查,并在人工批准后才签发认证插件标识。认证绑定到已审核的插件版本与校验和。请让工作区配置、存储、凭据、日志和后台工作都与当前安装绑定,不要依赖可变的进程全局状态。
如果审核发现该版本无法满足要求,请先作为未签名的普通专属插件提交,并在后续不可变版本中修复问题。不存在专属运行认证或中间认证等级。用户侧说明见[认证插件](/zh/plugin/certified-plugins)。
<img width="600" src="/images/zh/plugin/dev/plugin_submission.png" />
发布流程本身不涉及认证:插件默认以专属模式运行。如果这个版本还要申请认证,见[认证插件与共享运行](/zh/plugin/certified-plugins)。
有任何问题,您可以发送邮件到`[email protected]`或通过[社区](/zh/insight/community)联系我们。