Clawnet 重构(协议 + 认证统一)

嗨 嗨 Peter — 方向很好;这将解锁更简单的用户体验 + 更强的安全性。 目的 单一、严谨的文档用于: 当前状态:协议、流程、信任边界。 痛点:审批、多跳路由、UI 重复。 提议的新状态:一个协议、作用域角色、统一的认证/配对、TLS 固定。 身份模型:稳定 ID + 可爱的别名。 迁移计划、风险、开放问题。 目标(来自讨论) 所有客户端使用一个协议(mac 应用、CLI、iOS、Android、无头节点)。 每个网络参与者都经过认证 + 配对。 角色清晰:节点 vs 操作者。 中央审批路由到用户所在位置。 所有远程流量使用 TLS 加密 + 可选固定。 最小化代码重复。 单台机器应该只显示一次(无 UI/节点重复条目)。 非目标(明确) 移除能力分离(仍需要最小权限)。 不经作用域检查就暴露完整的 Gateway 网关控制平面。 使认证依赖于人类标签(别名仍然是非安全性的)。 当前状态(现状) 两个协议 1) Gateway 网关 WebSocket(控制平面) 完整 API 表面:配置、渠道、模型、会话、智能体运行、日志、节点等。 默认绑定:loopback。通过 SSH/Tailscale 远程访问。 认证:通过 connect 的令牌/密码。 无 TLS 固定(依赖 loopback/隧道)。 代码: src/gateway/server/ws-connection/message-handler.ts src/gateway/client.ts docs/gateway/protocol.md 2) Bridge(节点传输) 窄允许列表表面,节点身份 + 配对。 TCP 上的 JSONL;可选 TLS + 证书指纹固定。 TLS 在设备发现 TXT 中公布指纹。 代码: src/infra/bridge/server/connection.ts src/gateway/server-bridge.ts src/node-host/bridge-client.ts docs/gateway/bridge-protocol.md 当前的控制平面客户端 CLI → 通过 callGateway(src/gateway/call.ts)连接 Gateway 网关 WS。 macOS 应用 UI → Gateway 网关 WS(GatewayConnection)。 Web 控制 UI → Gateway 网关 WS。 ACP → Gateway 网关 WS。 浏览器控制使用自己的 HTTP 控制服务器。 当前的节点 macOS 应用在节点模式下连接到 Gateway 网关 bridge(MacNodeBridgeSession)。 iOS/Android 应用连接到 Gateway 网关 bridge。 配对 + 每节点令牌存储在 Gateway 网关上。 当前审批流程(exec) 智能体通过 Gateway 网关使用 system.run。 Gateway 网关通过 bridge 调用节点。 节点运行时决定审批。 UI 提示由 mac 应用显示(当节点 == mac 应用时)。 节点向 Gateway 网关返回 invoke-res。 多跳,UI 绑定到节点主机。 当前的在线状态 + 身份 来自 WS 客户端的 Gateway 网关在线状态条目。 来自 bridge 的节点在线状态条目。 mac 应用可能为同一台机器显示两个条目(UI + 节点)。 节点身份存储在配对存储中;UI 身份是分开的。 问题/痛点 需要维护两个协议栈(WS + Bridge)。 远程节点上的审批:提示出现在节点主机上,而不是用户所在位置。 TLS 固定仅存在于 bridge;WS 依赖 SSH/Tailscale。 身份重复:同一台机器显示为多个实例。 角色模糊:UI + 节点 + CLI 能力没有明确分离。 提议的新状态(Clawnet) 一个协议,两个角色 带有角色 + 作用域的单一 WS 协议。 ...

Exec 主机重构计划

目标 添加 exec.host + exec.security 以在沙箱、Gateway 网关和节点之间路由执行。 保持默认安全:除非明确启用,否则不进行跨主机执行。 将执行拆分为无头运行器服务,通过本地 IPC 连接可选的 UI(macOS 应用)。 提供每智能体策略、允许列表、询问模式和节点绑定。 支持与或不与允许列表一起使用的询问模式。 跨平台:Unix socket + token 认证(macOS/Linux/Windows 一致性)。 非目标 无遗留允许列表迁移或遗留 schema 支持。 节点 exec 无 PTY/流式传输(仅聚合输出)。 除现有 Bridge + Gateway 网关外无新网络层。 决定(已锁定) 配置键: exec.host + exec.security(允许每智能体覆盖)。 提升: 保留 /elevated 作为 Gateway 网关完全访问的别名。 询问默认: on-miss。 批准存储: ~/.openclaw/exec-approvals.json(JSON,无遗留迁移)。 运行器: 无头系统服务;UI 应用托管 Unix socket 用于批准。 节点身份: 使用现有 nodeId。 Socket 认证: Unix socket + token(跨平台);如需要稍后拆分。 节点主机状态: ~/.openclaw/node.json(节点 id + 配对 token)。 macOS exec 主机: 在 macOS 应用内运行 system.run;节点主机服务通过本地 IPC 转发请求。 无 XPC helper: 坚持使用 Unix socket + token + 对等检查。 关键概念 主机 sandbox:Docker exec(当前行为)。 gateway:在 Gateway 网关主机上执行。 node:通过 Bridge 在节点运行器上执行(system.run)。 安全模式 deny:始终阻止。 allowlist:仅允许匹配项。 full:允许一切(等同于提升模式)。 询问模式 off:从不询问。 on-miss:仅在允许列表不匹配时询问。 always:每次都询问。 询问独立于允许列表;允许列表可与 always 或 on-miss 一起使用。 ...

严格配置验证(仅通过 doctor 进行迁移)

目标 在所有地方拒绝未知配置键(根级 + 嵌套)。 拒绝没有 schema 的插件配置;不加载该插件。 移除加载时的旧版自动迁移;迁移仅通过 doctor 运行。 启动时自动运行 doctor(dry-run);如果无效,阻止非诊断命令。 非目标 加载时的向后兼容性(旧版键不会自动迁移)。 静默丢弃无法识别的键。 严格验证规则 配置必须在每个层级精确匹配 schema。 未知键是验证错误(根级或嵌套都不允许透传)。 plugins.entries.<id>.config 必须由插件的 schema 验证。 如果插件缺少 schema,拒绝插件加载并显示清晰的错误。 未知的 channels.<id> 键是错误,除非插件清单声明了该渠道 id。 所有插件都需要插件清单(openclaw.plugin.json)。 插件 schema 强制执行 每个插件为其配置提供严格的 JSON Schema(内联在清单中)。 插件加载流程: 解析插件清单 + schema(openclaw.plugin.json)。 根据 schema 验证配置。 如果缺少 schema 或配置无效:阻止插件加载,记录错误。 错误消息包括: 插件 id 原因(缺少 schema / 配置无效) 验证失败的路径 禁用的插件保留其配置,但 Doctor + 日志会显示警告。 Doctor 流程 每次加载配置时都会运行 Doctor(默认 dry-run)。 如果配置无效: 打印摘要 + 可操作的错误。 指示:openclaw doctor --fix。 openclaw doctor --fix: 应用迁移。 移除未知键。 写入更新后的配置。 命令门控(当配置无效时) 允许的命令(仅诊断): ...

出站会话镜像重构(Issue #1520)

状态 进行中。 核心 + 插件渠道路由已更新以支持出站镜像。 Gateway 网关发送现在在省略 sessionKey 时派生目标会话。 背景 出站发送被镜像到当前智能体会话(工具会话键)而不是目标渠道会话。入站路由使用渠道/对等方会话键,因此出站响应落在错误的会话中,首次联系的目标通常缺少会话条目。 目标 将出站消息镜像到目标渠道会话键。 在缺失时为出站创建会话条目。 保持线程/话题作用域与入站会话键对齐。 涵盖核心渠道加内置扩展。 实现摘要 新的出站会话路由辅助器: src/infra/outbound/outbound-session.ts resolveOutboundSessionRoute 使用 buildAgentSessionKey(dmScope + identityLinks)构建目标 sessionKey。 ensureOutboundSessionEntry 通过 recordSessionMetaFromInbound 写入最小的 MsgContext。 runMessageAction(发送)派生目标 sessionKey 并将其传递给 executeSendAction 进行镜像。 message-tool 不再直接镜像;它只从当前会话键解析 agentId。 插件发送路径使用派生的 sessionKey 通过 appendAssistantMessageToSessionTranscript 进行镜像。 Gateway 网关发送在未提供时派生目标会话键(默认智能体),并确保会话条目。 线程/话题处理 Slack:replyTo/threadId -> resolveThreadSessionKeys(后缀)。 Discord:threadId/replyTo -> resolveThreadSessionKeys,useSuffix=false 以匹配入站(线程频道 id 已经作用域会话)。 Telegram:话题 ID 通过 buildTelegramGroupPeerId 映射到 chatId:topic:<id>。 涵盖的扩展 Matrix、MS Teams、Mattermost、BlueBubbles、Nextcloud Talk、Zalo、Zalo Personal、Nostr、Tlon。 注意: Mattermost 目标现在为私信会话键路由去除 @。 Zalo Personal 对 1:1 目标使用私信对等方类型(仅当存在 group: 时才使用群组)。 BlueBubbles 群组目标去除 chat_* 前缀以匹配入站会话键。 Slack 自动线程镜像不区分大小写地匹配频道 id。 Gateway 网关发送在镜像前将提供的会话键转换为小写。 决策 Gateway 网关发送会话派生:如果提供了 sessionKey,则使用它。如果省略,从目标 + 默认智能体派生 sessionKey 并镜像到那里。 会话条目创建:始终使用 recordSessionMetaFromInbound,Provider/From/To/ChatType/AccountId/Originating* 与入站格式对齐。 目标规范化:出站路由在可用时使用解析后的目标(resolveChannelTarget 之后)。 会话键大小写:在写入和迁移期间将会话键规范化为小写。 添加/更新的测试 src/infra/outbound/outbound-session.test.ts Slack 线程会话键。 Telegram 话题会话键。 dmScope identityLinks 与 Discord。 src/agents/tools/message-tool.test.ts 从会话键派生 agentId(不传递 sessionKey)。 src/gateway/server-methods/send.test.ts 在省略时派生会话键并创建会话条目。 待处理项目 / 后续跟进 语音通话插件使用自定义的 voice:<phone> 会话键。出站映射在这里没有标准化;如果 message-tool 应该支持语音通话发送,请添加显式映射。 确认是否有任何外部插件使用内置集之外的非标准 From/To 格式。 涉及的文件 src/infra/outbound/outbound-session.ts src/infra/outbound/outbound-send-service.ts src/infra/outbound/message-action-runner.ts src/agents/tools/message-tool.ts src/gateway/server-methods/send.ts 测试: src/infra/outbound/outbound-session.test.ts src/agents/tools/message-tool.test.ts src/gateway/server-methods/send.test.ts

插件 SDK + 运行时重构计划

目标:每个消息连接器都是一个插件(内置或外部),使用统一稳定的 API。 插件不直接从 src/** 导入任何内容。所有依赖项均通过 SDK 或运行时获取。 为什么现在做 当前连接器混用多种模式:直接导入核心模块、仅 dist 的桥接方式以及自定义辅助函数。 这使得升级变得脆弱,并阻碍了干净的外部插件接口。 目标架构(两层) 1)插件 SDK(编译时,稳定,可发布) 范围:类型、辅助函数和配置工具。无运行时状态,无副作用。 内容(示例): 类型:ChannelPlugin、适配器、ChannelMeta、ChannelCapabilities、ChannelDirectoryEntry。 配置辅助函数:buildChannelConfigSchema、setAccountEnabledInConfigSection、deleteAccountFromConfigSection、 applyAccountNameToChannelSection。 配对辅助函数:PAIRING_APPROVED_MESSAGE、formatPairingApproveHint。 新手引导辅助函数:promptChannelAccessConfig、addWildcardAllowFrom、新手引导类型。 工具参数辅助函数:createActionGate、readStringParam、readNumberParam、readReactionParams、jsonResult。 文档链接辅助函数:formatDocsLink。 交付方式: 以 openclaw/plugin-sdk 发布(或从核心以 openclaw/plugin-sdk 导出)。 使用语义化版本控制,提供明确的稳定性保证。 2)插件运行时(执行层,注入式) 范围:所有涉及核心运行时行为的内容。 通过 OpenClawPluginApi.runtime 访问,确保插件永远不会导入 src/**。 建议的接口(最小但完整): export type PluginRuntime = { channel: { text: { chunkMarkdownText(text: string, limit: number): string[]; resolveTextChunkLimit(cfg: OpenClawConfig, channel: string, accountId?: string): number; hasControlCommand(text: string, cfg: OpenClawConfig): boolean; }; reply: { dispatchReplyWithBufferedBlockDispatcher(params: { ctx: unknown; cfg: unknown; dispatcherOptions: { deliver: (payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; }) => void | Promise<void>; onError?: (err: unknown, info: { kind: string }) => void; }; }): Promise<void>; createReplyDispatcherWithTyping?: unknown; // adapter for Teams-style flows }; routing: { resolveAgentRoute(params: { cfg: unknown; channel: string; accountId: string; peer: { kind: RoutePeerKind; id: string }; }): { sessionKey: string; accountId: string }; }; pairing: { buildPairingReply(params: { channel: string; idLine: string; code: string }): string; readAllowFromStore(channel: string): Promise<string[]>; upsertPairingRequest(params: { channel: string; id: string; meta?: { name?: string }; }): Promise<{ code: string; created: boolean }>; }; media: { fetchRemoteMedia(params: { url: string }): Promise<{ buffer: Buffer; contentType?: string }>; saveMediaBuffer( buffer: Uint8Array, contentType: string | undefined, direction: "inbound" | "outbound", maxBytes: number, ): Promise<{ path: string; contentType?: string }>; }; mentions: { buildMentionRegexes(cfg: OpenClawConfig, agentId?: string): RegExp[]; matchesMentionPatterns(text: string, regexes: RegExp[]): boolean; }; groups: { resolveGroupPolicy( cfg: OpenClawConfig, channel: string, accountId: string, groupId: string, ): { allowlistEnabled: boolean; allowed: boolean; groupConfig?: unknown; defaultConfig?: unknown; }; resolveRequireMention( cfg: OpenClawConfig, channel: string, accountId: string, groupId: string, override?: boolean, ): boolean; }; debounce: { createInboundDebouncer<T>(opts: { debounceMs: number; buildKey: (v: T) => string | null; shouldDebounce: (v: T) => boolean; onFlush: (entries: T[]) => Promise<void>; onError?: (err: unknown) => void; }): { push: (v: T) => void; flush: () => Promise<void> }; resolveInboundDebounceMs(cfg: OpenClawConfig, channel: string): number; }; commands: { resolveCommandAuthorizedFromAuthorizers(params: { useAccessGroups: boolean; authorizers: Array<{ configured: boolean; allowed: boolean }>; }): boolean; }; }; logging: { shouldLogVerbose(): boolean; getChildLogger(name: string): PluginLogger; }; state: { resolveStateDir(cfg: OpenClawConfig): string; }; }; 备注: ...