OpenClaw Channel Ingress API:入站消息访问控制边界的接入指南
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
Channel ingress(通道入站)是 OpenClaw 中针对入站通道事件的实验性访问控制边界:通道插件负责提供平台事实(发送者身份、会话类型、路由归属等),而 OpenClaw 内核负责通用策略裁决,包括 DM/群组白名单、配对存储条目、路由门禁、命令门禁、事件认证、@提及激活与准入门决(admission)。本文基于 OpenClaw 官方文档 Channel ingress API 展开,结合 Plugin SDK 子路径实现 与 标识认证实现 等仓库源码,完整讲解openclaw/plugin-sdk/channel-ingress-runtime的用法、结果投影结构、IdentifierAuthentication标识认证等级、访问组、事件模式、路由与激活门禁,以及脱敏与安全审计边界,帮助通道插件开发者把接收路径(receive path)正确接入这一统一的准入门槛。
职责划分:插件提供事实,内核负责策略
文档开头明确了 ingress 的边界定位:插件拥有平台事实与副作用,内核拥有通用策略。具体来说,内核侧的策略面覆盖:
- DM/群组白名单(allowlists)与配对存储(pairing-store)中的 DM 条目;
- 路由门禁(route gates)与命令门禁(command gates);
- 事件认证(event auth)、@提及激活(mention activation);
- 脱敏诊断(redacted diagnostics)与准入门决(admission)。
这条分工决定了通道插件的接收路径不应该自行拼装"最终白名单"或"命令属主",而应当把原始事实交给解析器,由解析器统一推导。使用入口是 SDK 子路径openclaw/plugin-sdk/channel-ingress-runtime,该子路径在 package.json 的 exports 中注册,指向src/plugin-sdk/channel-ingress-runtime.ts。
从 channel-ingress-runtime.ts 的源码结构看,这个子路径是一个"门面"模块:它把内核src/channels/message-access/下的多个实现模块重新导出为插件可用 API——
channelIngressRoutes、createChannelIngressResolver、resolveChannelMessageIngress、resolveStableChannelMessageIngress来自 runtime.ts;meetsIdentifierAuthentication与类型IdentifierAuthentication来自 identifier-authentication.ts;defineStableChannelIngressIdentity、identityEntryAuthenticationClassifier来自 runtime-identity.ts;readChannelIngressStoreAllowFromForDmPolicy来自 store-allow-from.ts;resolveChannelImplicitMentions来自 implicit-mentions 配置模块。
仓库中已有不少捆绑通道实际接入了该子路径,例如 WhatsApp 准入门、Signal 事件处理器、IRC 入站、飞书 ingress 等,可作为真实接入范本。
运行时解析器:resolveChannelMessageIngress
典型的接收路径接入代码如下(完整继承自官方文档,参数含义见注释):
import { defineStableChannelIngressIdentity, resolveChannelMessageIngress, } from "openclaw/plugin-sdk/channel-ingress-runtime"; // 1) 声明稳定的入站身份描述符:key 是身份字段名,normalize 做归一化, // sensitivity 标记敏感度(此处为 pii)。 const identity = defineStableChannelIngressIdentity({ key: "platform-user-id", normalize: normalizePlatformUserId, sensitivity: "pii", }); // 2) 解析单条入站消息的准入决策。 const result = await resolveChannelMessageIngress({ channelId: "my-channel", accountId, identity, subject: { stableId: platformUserId }, // 归一化后的发送者身份 conversation: { kind: isGroup ? "group" : "direct", id: conversationId }, contextBinding: { agentId: agentRoute.agentId, sessionKey: agentRoute.sessionKey, messageId, inboundEventKind: "user_request", // 入站事件类型 }, event: { kind: "message", authMode: "inbound", mayPair: !isGroup }, policy: { dmPolicy: config.dmPolicy, groupPolicy: config.groupPolicy, groupAllowFromFallbackToAllowFrom: true, }, allowFrom: config.allowFrom, // 原始白名单(不预计算) groupAllowFrom: config.groupAllowFrom, accessGroups: cfg.accessGroups, route, readStoreAllowFrom, command: hasControlCommand ? { allowTextCommands: true, hasControlCommand } : undefined, }); // 3) 把解析结果原样交给宿主注入的上下文构建器。 const ctx = runtime.channel.inbound.buildContext({ // 传入精确的宿主结果;不要基于 SenderId、From、会话键、路由、 // 房间或消息元数据自行重建参与者证据。 channelIngress: result, // ...归一化后的通道事实 });调用约束有三条,均直接影响结果的正确性与可追溯性:
- 不要预计算有效白名单、命令属主或命令组。解析器从原始白名单、存储回调、路由描述符、访问组、策略与会话类型自行推导这些值。
contextBinding的时机。若结果要进入宿主上下文,必须在通道路由属主选定最终 agent 与 session 之后再解析;contextBinding会将这些事实连同稳定的传输层 message id(如存在)与最终入站事件类型一起冻结。仅做决策检查(decision-only)时可以省略它,但这样的结果不构成合法的执行出处(execution provenance),不得作为channelIngress传入。- 批量消息的顺序。当通道一次批量放行多条消息时,必须按源顺序传入各自的精确结果;最终上下文的 message id 标识的是最后一条源结果。
产品参与者身份(Product participant identity)
身份描述符可以提供resolveParticipant(subject),仅当插件能够证明相应远程事实时返回{ domain, idKind, id }。语义要点:
domain属于远程服务侧,例如 Slack 工作区或应用范围内的身份签发方,不是OpenClaw 本地的accountId;- 当服务对它们赋予不同含义时,用户 ID、机器人 ID、代理身份必须保持区分;
- 昵称和"Gateway 档案查找成功"不构成身份证据。
传递路径要求:把解析器的精确结果原样交给宿主注入的上下文构建器。宿主会私下携带产品事实,直到被接受的输入被记录进会话参与者聚合(session participant aggregate);原始产品身份不会进入公开的诊断结果。未被限定(unqualified)的生产者保持为"未解析观察",而不会变成 Gateway 档案或凭空捏造的远程主体。产品参与(product participation)本身不授予任何访问权限。
此外,这条产品路径与执行身份审计收集是相互独立的:它既不开启审计收集,也不复用其 HMAC 引用或不透明诊断载体。
结果投影:直接消费现代字段
捆绑插件应当直接消费现代投影字段,而不是把结果再翻译回本地 DTO:
| 字段 | 含义 |
|---|---|
ingress | 有序的 gate 决策与准入结果 |
senderAccess | 仅发送者/会话授权 |
routeAccess | 路由与路由-发送者投影 |
commandAccess | 命令授权;未运行命令 gate 时requested: false |
activationAccess | @提及/激活结果 |
事件授权不单独输出投影,仍保留在有序的ingress.graph与决定性的ingress.reasonCode上。文档同时声明:废弃的第三方 SDK 助手可能内部重建旧结构,但新的捆绑接收路径不应再把现代结果翻译回本地 DTO。
执行身份审计与boundary-verified
当执行身份审计收集开启时,受信的原生活动插件是其远程参与者事实的权威进程内生产者。宿主注入的注册运行时会把解析器结果绑定到确切的插件记录与注册表生命周期 epoch(epoch),并在一次性(one-shot)上下文交接中校验其完整可用的会话、路由、agent、会话、消息、事件与参与者范围。公开的独立构建器(standalone builder)保持非权威,不能铸造参与者证据。队列收集只有在每个贡献都对同一参与者具备有效证据时才保留归因;混合、缺失、过期或未铸造的证据一律记为unknown。载体是不透明、有界、一次性、仅用于诊断的;插件无法基于调用方选择的 sender、account、room、route、session、message 或 transport 字段铸造证据。SDK 有意不暴露任何记录、epoch、属主能力、参与者证据构造函数或证据拷贝器。
对boundary-verified的准确理解是:内核验证了该参与者事实携带确切的记录、epoch、范围与一次性交接穿过了这条受信的原生注册插件边界;它不表示内核独立查询了远程服务——只有通道插件能观察到那个传输层事实。
三种审计状态彼此独立且都不得被误读为"放行":
- supported:权威 ingress 解析器已运行,其精确结果可以产出存在(present)的调用者与"强制执行或仅归因"级别的覆盖;
- unknown:受支持的交接缺失、过期、伪造、复用、混合或未能通过宿主校验。
unknown绝不意味着允许; - unsupported:某命名路径没有权威的 ingress 解析器集成,显式传入
channelIngress: "unsupported"。unsupported绝不意味着允许,也不是接线不完整的捷径。
标识认证:IdentifierAuthentication
IdentifierAuthentication把一个标识符声明分级为verified、asserted、unverified、mutable,由强到弱。它是通道授权的输入,不是主体(principal)、授权(grant)、关系或执行身份保障强度——尤其地,verified的标识声明永远不会变成执行保障中的boundary-verified或cryptographic。
在仓库实现中,identifier-authentication.ts 给出了四级等级表(verified: 3, asserted: 2, unverified: 1, mutable: 0)、默认值asserted,以及布尔比较器meetsIdentifierAuthentication(actual, minimum)。文档要求下游认证映射器使用该比较器,而不要各自维护 rank 表。
四个等级的规范性含义:
verified:拥有该标识的受信传输或会话边界把这个精确标识绑定到了该发送者;asserted:受信边界为该发送者作了担保,但没有绑定这个精确标识;unverified:标识精确且稳定,但未证明其归属声明;mutable:标识是可更改或共享的别名,例如显示名。
只允许基于"由拥有边界控制的传输或会话元数据"声明verified。发送者可控内容、模型输入、普通消息上下文、路由元数据以及宿主准入载体的完整性都不能建立verified。
内核的配对与比较逻辑
内核保留精确匹配的"脱敏白名单条目 ↔ 主体标识"配对,取该精确配对中较弱的一级再与minIdentifierAuthentication比较。同种类型的标识保持相互独立,因此一个弱化的备用邮箱不会削弱另一个独立命中的已验证邮箱。
若主体按消息提供authentication映射表,它必须声明每一个希望被计数的字段;映射表中缺失的字段按unverified处理,即使其身份描述符声明了更强的静态断言。强度静态不变的通道则完全省略该映射表。
安全审计:classifyEntryAuthentication
从openclaw/plugin-sdk/channel-ingress-runtime导入identityEntryAuthenticationClassifier,并把classifyEntryAuthentication: identityEntryAuthenticationClassifier(identity)暴露在安全适配器的resolveDmPolicy结果上。它使用身份描述符的条目归一化器,返回接受字段中最强的静态断言;没有字段接受该条目时返回undefined,通配符条目被排除。安全审计(实现位于 audit-channel.ts)据此统计仅依赖可变标识配置的allowFrom条目:当名称匹配被禁用时告警,启用时则预览禁用名称匹配后有多少条目将停止授权。审计发现只包含计数与配置路径,不含原始条目;配对存储的批准不在此检查范围内。符号化的accessGroup:引用单独解析成员关系,不计为可变标识。
废弃字段的兼容映射
现有插件在废弃窗口内保持源码兼容,映射关系如下:
| 废弃字段 | 精确映射 |
|---|---|
dangerous: true | authentication: "mutable" |
dangerous: false或缺省 | 默认authentication: "asserted" |
mutableIdentifierMatching: "enabled" | 最低级别mutable |
mutableIdentifierMatching: "disabled"或缺省 | 默认最低级别asserted |
显式给出的authentication或minIdentifierAuthentication优先。identifier-authentication.ts 中的identifierAuthenticationFrom与minimumIdentifierAuthenticationFrom正是这两条兼容链的实现:dangerous: true映射为mutable,缺省回落asserted;mutableIdentifierMatching: "enabled"映射为mutable,其余回落默认值。这些废弃字段保留到当前 Plugin SDK 主版本,计划在捆绑与已知外部插件迁移后的下一主版本移除。
捆绑通道的声明强度
捆绑通道采用"所有共享同一身份声明的接收路径所支持的最强断言"。这些是通道授权声明,不是执行身份保障:
| 通道 | 标识声明 | 权威的传输或会话事实 |
|---|---|---|
| Discord | Gateway 用户 ID:verified;PluralKit 成员 ID:asserted;名称与 tag:mutable | Discord 在经 bot token 认证的 Gateway 会话投递的事件中提供author.id或user.id。PluralKit 成员 ID 来自其认证 API 响应,而非 Discord Gateway。 |
| Google Chat | sender.name:verified;邮箱:mutable | Webhook 在消费 Google 拥有的事件体之前校验 Google 签名 token、签发方与配置的受众。 |
| IRC | 服务器连接前缀与user@host:asserted;基于昵称的别名:mutable | 选定的 IRC 服务器担保连接前缀,但通用传输不证明账户归属。 |
| Mattermost | 帖子用户 ID:verified;用户名:mutable | 已认证的 Mattermost WebSocket 发出服务器拥有的帖子事件,其中post.user_id标识作者。 |
| Microsoft Teams | 发送者与会话 ID:asserted;发送者名称:mutable | Bot Framework 认证连接器活动,但插件不独立证明每个 ID 表示的精确归属。 |
| Slack | 用户与工作区用户 ID:asserted;名称与 slug:mutable | 直连 Slack 投递绑定用户 ID,而中继模式认证中继对端但没有端到端的精确发送者证明;共享声明采用可辩护的共同断言。 |
如果某条接收路径无法支撑其通道共享的声明,应当拆分声明或提供较弱的按消息断言。永远不要从消息文本、路由或宿主证据载体完整性推断更强的声明。
访问组(Access groups)
accessGroup:<name>条目保持脱敏。内核自行解析静态的message.senders组,只对需要平台查询的动态组调用resolveAccessGroupMembership。缺失、不支持或失败的组一律失败关闭(fail closed)。
事件模式(Event modes)
authMode决定入站事件走哪类门禁:
authMode | 含义 |
|---|---|
inbound | 常规入站发送者门禁 |
command | 面向回调或作用域按钮的命令门禁 |
origin-subject | 操作者必须与原始消息主体匹配 |
route-only | 仅对路由作用域的受信事件执行路由门禁 |
none | 插件自有的内部事件绕过共享认证 |
对 reactions、按钮、回调与原生命令,使用mayPair: false。
路由与激活(Routes and activation)
房间、话题、guild、线程或嵌套路由策略使用路由描述符表达:
route: { id: "room", allowed: roomAllowed, enabled: roomEnabled, senderPolicy: "replace", senderAllowFrom: roomAllowFrom, blockReason: "room_sender_not_allowlisted", }当插件有多个可选路由描述符时使用channelIngressRoutes(...):它会过滤禁用分支,同时保持路由事实通用,并按各描述符的precedence排序。
@提及门禁是一种激活门禁:提及未命中返回admission: "skip",使 turn 内核不处理仅观察(observe-only)的回合。大多数通道应让激活位于发送者与命令门禁之后;只有必须在发送者白名单噪音之前静默未提及流量的公共聊天界面,才可在禁用文本命令旁路时选择activation.order: "before-sender"。
对具有隐式激活的通道(例如机器人线程中的回复),先用resolveChannelImplicitMentions(...)解析channels.defaults.implicitMentions加上通道与账户级覆盖,再把结果作为activation.implicitMentions传入。投影字段activationAccess.shouldBypassMention报告命令或隐式激活何时旁路了显式提及。
脱敏(Redaction)
原始发送者值与原始白名单条目只是解析器输入。它们不得出现在解析后状态、决策、诊断、快照或兼容性事实中。应使用不透明的 subject id、entry id、route id 与 diagnostic id。
验证方式
文档给出的验证命令(在仓库根目录执行):
pnpm test src/channels/message-access/message-access.test.ts src/plugin-sdk/channel-ingress-runtime.test.ts pnpm plugin-sdk:api:diff --base "$(git merge-base origin/main HEAD)" --head HEAD两条测试均存在于当前仓库:message-access.test.ts 覆盖内核准入逻辑,channel-ingress-runtime.test.ts 覆盖 SDK 子路径行为。第二条命令用于在改动 SDK 时对比 API 差异,确认对外契约未意外变化。
相关文档
- Channel inbound API — 以
channelIngress消费本解析器结果的接收路径; - Channel outbound API — 同一通道插件的发送侧;
- Building channel plugins — 通道插件完整开发流程;
- 安全审计检查 —
classifyEntryAuthentication支撑的可变标识审计。
【免费下载链接】openclawThe AI that really does things. Any OS. Any Platform. The lobster way. 🦞项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考