LangBot Docs

认证插件与共享运行

认证是什么、为什么存在、什么时候需要,以及 shared-runtime-v1 / stateless-v1 的适配规范。

认证是什么

认证是 LangBot 对一份精确、不可变的插件制品授予的资格:该制品被批准在 LangBot Cloud 中跨租户共享运行。

  • 认证绑定到制品本身(已发布的具体版本,按 digest 区分),不是绑定到插件名或作者。同一个插件发布新版本后,上一版的认证不会自动延续。
  • 认证不是质量评分,也不是安全审计背书。

为什么需要认证

平台默认不让插件共享运行。这是因为共享的粒度是制品:

  • 专属模式:插件不参与共享,独立运行。
  • 共享模式:同一个 artifact digest 的所有安装共用一个 Worker。

共享的好处是进程数量和启动开销不随安装数线性增长。代价是一个进程里会同时处理多个工作区的调用——如果插件把租户数据放在实例上,就会在租户之间串数据。

所以平台需要先对这份制品做代码审查、确认它满足无状态约束,再用签名把"对这份制品的信任"固定下来,才敢让它进入共享放置。这就是认证存在的原因。

什么情况下需要认证

大多数插件不需要认证。 不认证不影响你把插件发布到插件市场,也不影响用户安装使用——插件会以专属模式正常运行。

你的情况是否需要认证
只想把插件发布到插件市场供用户使用不需要
插件需要在进程内保留跨调用状态(例如常驻的外部会话进程)不需要,也应该保持专属运行
插件里有后台任务、定时器,或长期缓存租户数据不需要,保持专属模式更安全
希望获得认证插件标识需要
希望同一制品的多个安装共用一个进程,降低资源与启动开销需要
插件面向 Cloud 多租户场景,希望被平台以共享方式放置需要

判断标准只有一条:只有当你的代码确实能做到"实例上不保存任何租户或安装相关数据"时,认证才有意义。

认证之后的效果

通过审核后:

  • 制品被签入证书,安装时以共享模式运行;同一 digest 的多个安装共用一个 Worker。
  • 该 Worker 中只有一个 BasePlugin 实例,以及每个已声明组件 manifest 各一个实例(EventListener、Tool、Command、KnowledgeEngine、Parser、Page、Runner)。
  • 每个安装只额外保留一份轻量的配置、权限与生命周期上下文,调用时由当前请求决定使用哪一份。
  • 你会获得认证插件标识。
  • 自托管 OSS 默认不配置认证签发方公钥,因此即使是带认证的制品,在自托管实例上仍以专属模式运行。

同时你也承担一项长期义务:后续每个版本都必须继续满足无状态约束。修改代码后需要提交新版本重新审核,认证不会自动延续。

需要做的适配与规范

声明

在 manifest.yaml 的 execution 下同时声明两个字段:

apiVersion: v1
kind: Plugin
metadata:
  author: your-name
  name: YourPlugin
  version: 0.1.1
spec:
  config: []
  components: {}
execution:
  python:
    path: main.py
    attr: YourPlugin
  sharedRuntime: shared-runtime-v1
  componentModel: stateless-v1
字段合法值说明
execution.sharedRuntimeshared-runtime-v1只接受这一个值,填写其他值会导致清单校验失败
execution.componentModelstateless-v1只接受这一个值
  • 两个字段必须同时声明。只写 sharedRuntime 不写 componentModel 的包会在认证校验阶段被拒绝。
  • 这两个字段自 SDK 0.7.0 起被识别;更早的 SDK 会忽略它们,因此不要用旧版本验证声明是否生效。

声明只是申请,不等于通过认证。它本身不会授予认证标识,也不会改变运行模式。

无状态组件规范

  • 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)。进程内缓存只允许存放制品级公共数据;如果确实要缓存租户数据,键必须包含完整安装标识,并能在安装被移除时清理。
  • 不要发起携带租户数据的后台任务或线程:SDK 不跟踪你自己创建的任务的所有权。如果确实要脱离处理函数运行,请以安装绑定为键维护一份登记表,并在 on_installation_revoked() 中停止它们。处理函数返回后,本次调用的上下文和运行关联调用即失效。

一个安装,多份用法

一个 workspace 内每个插件只安装一次,但同一份安装会被多个产品对象复用:

组件类型复用它的是隔离靠什么
Agent / 流水线中的 Runner每个处理器ctx.state.* 已按处理器隔离(作用域含 binding_identity,由 Agent UUID 派生)
知识库用的 KnowledgeEngine / Parser每个知识库需要插件自己按知识库 ID 隔离
Tool / Command / EventListener / Page多个处理器、会话或页面请求依赖调用时读取,不能在实例上累积

平台的插件存储键是 (实例, 工作区, 组件类型, 插件名, key),不包含处理器或知识库维度。所以只要你的插件要持久化或缓存,就必须自己把产品对象的身份拼进键里:

  • 配置:每次调用读取(ctx.config,或 task-local 的 self.get_plugin_config()),不要缓存到实例。
  • 持久化:键必须包含产品对象身份(处理器身份 / 知识库 ID / 会话身份)。
  • 进程内缓存:键必须同时包含完整安装绑定和产品对象身份,并在安装被撤销时释放(见下)。
  • 锁与栅栏:必须按 (安装, 产品对象) 记。一个处理器或知识库的失败,不得阻断同一安装下的其他对象。
  • 失败分级:只有"已经派发、结果未知"的变更才应进入需要人工对账的状态;确定性的校验错误和调用前的取消不应影响后续调用。
  • 日志与遥测:诊断数据的失败不得影响业务,也不要把租户数据写进进程级单例。

安装被撤销时

共享模式下,一个进程内只有一个对象图,服务同一制品的所有安装。所以某个安装被撤销时对象图不会销毁,插件必须自己释放按安装绑定记的进程内状态:

class MyPlugin(BasePlugin):
    def __init__(self) -> None:
        super().__init__()
        self._caches: dict[str, Cache] = {}

    async def on_installation_revoked(self, binding) -> None:
        self._caches.pop(binding.installation_uuid, None)
  • 触发时机:卸载、停用、移除工作区,以及升级(旧绑定被撤销)。只有共享模式会调用;专属模式下进程会被关停,进程内状态随进程消失。
  • 调用时没有调用上下文,Host API(存储、配置、绑定查询)不可用——只能释放进程内状态,安装身份由参数给出。
  • 抛出异常会被记录,不影响撤销流程。
  • 需要带 BasePlugin.on_installation_revoked 的 SDK 版本;旧版本上这个钩子只是永远不会被调用。

允许 / 禁止对照

场景允许禁止
读取配置每次调用时读取(self.get_plugin_config()、ctx.config)在 initialize() 中读取后保存到 self
连接池 / HTTP 客户端不含租户凭据的共享连接池把安装的 endpoint、api key 固化到实例上
缓存制品级公共数据;或键为完整安装标识且支持清理模块级 dict 以工作区或用户为键长期驻留
跨调用状态Host 服务(Runner 用 ctx.state_*)实例字段、模块级全局变量
后台工作无处理函数返回后继续持有本次上下文或其关联调用
日志ctx.log() 或 SDK 日志 API模块级后台线程直接输出租户配置或凭据

常见反例

1. 在初始化时读取安装配置

async def initialize(self):
    self.endpoint = self.plugin.config["endpoint"]  # 进程级初始化,没有当前安装
    self.api_key = self.plugin.config["api_key"]

共享初始化阶段没有安装配置,这段代码只会读到空值或上一次的值。

2. 把租户数据放进模块级缓存

_CACHE = {}  # 以工作区或用户为键,会跨安装、跨租户长期驻留

async def call(self, params, **kwargs):
    if params["user"] not in _CACHE:
        _CACHE[params["user"]] = await fetch(params["user"])

3. 处理函数返回后继续使用本次调用

async def call(self, params, **kwargs):
    config = self.get_plugin_config()
    asyncio.create_task(self._upload_later(config))  # 处理函数已返回,上下文与授权均已失效
    return ok()

4. 把实例字段当作配置容器

async def initialize(self):
    self.default_endpoint = "https://api.example.com"  # 公共常量没有问题
    self.last_config = {}  # 一旦写入安装配置就违规

正确写法:

class LookupTool(Tool):
    async def initialize(self):
        # 只放置制品级、不含租户信息的对象
        self.transport = StatelessHTTPTransport()

    async def call(self, params, **kwargs):
        config = self.get_plugin_config()  # 每次调用按当前安装读取
        return await self.transport.lookup(config["endpoint"], params)

Runner 组件特别注意

Runner 与无状态约束的张力最大,因为运行器天然倾向于在进程内保存会话与上下文。

  • RunnerContext 只在本次运行期间有效,不要把它或它的字段保存到实例上跨调用复用。
  • Runner 需要跨调用保留的会话状态,走 ctx.state_get() / ctx.state_set()(作用域支持 conversation、actor、subject、runner)或 Host 提供的其他存储,不要使用实例字段。
  • 处理函数返回后,不要继续在后台任务中使用 ctx,也不要继续使用通过 self.plugin 发起的运行关联调用。
  • 如果 Runner 的设计必须依赖进程内持有的跨调用状态(例如需要长期驻留的外部会话进程),不要声明 stateless-v1,继续以专属 Worker 运行即可。

运行上下文的完整接口见运行上下文(RunnerContext)。

自检与提交

提交审核前逐条核对:

  • manifest.yaml 中两个字段都已声明,取值正确。
  • 全文搜索,确认没有模块级可变全局变量。
  • 确认 initialize() 只做进程级初始化,不读取安装配置。
  • 确认所有安装配置都在调用时读取。
  • 确认没有在处理函数返回后继续运行的任务或线程。
  • 确认跨调用状态通过 Host API 保存(Runner 使用 ctx.state_*)。

提交方式

认证与发布走同一条链路,不需要额外流程:在 manifest.yaml 中声明上面两个字段,然后照常用 lbp publish 提交一个新版本(已发布的版本无法追加声明)。LangBot Space 会检查本次提交的精确插件包,进行辅助代码审查,并在人工批准后签发认证插件标识;认证绑定到已审核的版本与校验和。

如果审核发现该版本无法满足要求,请先把它作为未签名的普通插件发布,并在后续不可变版本中修复问题。

如果核对下来发现插件并不适合共享运行,直接以普通插件提交即可,不需要认证。发布流程见发布到插件市场。

On this page