OpenClaw 聊天消息安全模型

基于源码,安全模型可以分为 8 层,从外到内逐层保护。

全景图


消息进入
  │
  ▼
[第1层] 接入控制 ── 发送者在白名单?配对通过?DM/群策略允许?
  │ ✗ → 拒绝
  ▼
[第2层] 会话隔离 ── 路由到正确的 agent session,防跨用户泄露
  │
  ▼
[第3层] 命令授权 ── 有权限执行此命令?需要 @mention?
  │ ✗ → 忽略/拒绝
  ▼
[第4层] 输入消毒 ── 剥离控制字符,外部内容加边界标记 + 注入检测
  │
  ▼
[第5层] 工具策略 ── Agent 的 tool profile 允许此工具?deny list 拦截?
  │ ✗ → 工具不可用
  ▼
[第6层] Exec 审批 ── 命令在白名单?是 safe bin?需要人工审批?
  │ deny → 不执行
  ▼
[第7层] 秘密保护 ── 日志脱敏、环境变量过滤、配置遮盖
  │
  ▼
[第8层] 传输安全 ── TLS、timing-safe 认证、文件权限
  │
  ▼
  安全执行 & 响应

核心原则:deny-by-default,逐层放行,deny 优先于 allow

第 1 层:谁能跟 Bot 说话(接入控制)

核心文件: src/channels/allow-from.ts, src/security/dm-policy-shared.ts

有两种场景:私聊 (DM)群聊 (Group),各有独立策略:

策略DM 行为群聊行为
`disabled`完全拒绝完全拒绝
`allowlist`仅白名单用户仅白名单用户
`pairing`需交互式配对审批(仅 DM)
`open`任何人都能聊任何人都能聊

白名单匹配

核心文件: src/channels/allowlist-match.ts

allowFrom 配置详解

allowFrom 是第 1 层的核心参数——白名单本体。消息进来第一件事就查 allowFrom,不在列表里的直接丢弃,连 session 都不会创建,后面 7 层全不走。


# 典型配置
channels:
  discord:
    allowFrom: ["384619457508540416"]  # Discord 用户 ID
  telegram:
    allowFrom: ["@frankyoung2024"]     # Telegram 用户名

allowFromdmPolicy 的关系

dmPolicyallowFrom 作用效果
`allowlist`只放行列表中的人最严格,生产环境推荐
`pairing`陌生人走配对流程,通过后等效加入白名单适合需要动态添加用户的场景
`open`allowFrom 被忽略,所有人都能聊公开 Bot(客服等)
`disabled`谁都不行完全关闭 DM

allowFrom: ["*"] 等效于 dmPolicy: "open",任何人都能私聊 Bot。

配对系统

核心文件: src/pairing/pairing-challenge.ts

第 2 层:会话隔离(防消息泄露)

核心文件: src/routing/resolve-route.ts, src/routing/session-key.ts

不同用户的对话必须隔离。假设 Alice 和 Bob 都在跟同一个 Bot 私聊,如果没有会话隔离,Alice 的敏感信息可能被 Bob 通过 context window 间接获取——共享会话就是信息泄露

2a. Session Key 生成(隔离粒度)

每条消息到达时,系统用一组维度生成 session key,决定这条消息进入哪个会话:


session key = f(agent, channel, account, group, peer)

DM 隔离级别可配(dmSessionIsolation):

级别Session Key 示例效果适用场景
`main``agent:main:main`所有 DM 共享一个会话个人使用,只有自己跟 Bot 聊
`per-peer``agent:main:dm:{peer_id}`每个发送者独立会话多人使用同一个 Bot(如客服)
`per-channel-peer``agent:main:{channel}:dm:{peer_id}`频道+发送者隔离Bot 同时接 Telegram + Discord
`per-account-channel-peer``agent:main:{account}:{channel}:dm:{peer_id}`账号+频道+发送者多 Bot 账号场景(最严格)

main 模式的安全警告:如果检测到多个不同用户发 DM,系统会在日志中警告——因为他们实际上在共享 context window,能"看到"彼此的对话历史。

2b. 路由绑定(消息送到哪个 Agent)

核心文件: src/routing/resolve-route.ts

一个 OpenClaw 实例可以跑多个 Agent。路由系统按优先级匹配决定消息发给谁:


消息到达
  │
  ├─ 匹配 peer binding?(特定用户 → 特定 agent)
  │   └─ 是 → 路由到绑定的 agent
  │
  ├─ 匹配 guild + role binding?(Discord 服务器+角色)
  │   └─ 是 → 路由到绑定的 agent
  │
  ├─ 匹配 team binding?
  │   └─ 是 → ...
  │
  ├─ 匹配 account binding?(哪个 Bot 账号收到的)
  │   └─ 是 → ...
  │
  ├─ 匹配 channel binding?(哪个频道)
  │   └─ 是 → 如 #deep-research → researcher agent
  │
  └─ 都不匹配 → default agent(通常是 main)

优先级从高到低:peer > guild+roles > team > account > channel > default

2c. 跨会话隔离边界

Agent 之间的隔离是硬性的:

唯一的跨会话通道是显式的、受控的:

这些都是可审计的跨界操作,不是隐式的数据共享。

2d. 实际场景举例

场景 1:客服 Bot(一个 agent 服务多客户)


客户 Alice DM → session: agent:support:dm:alice_123
  "我的订单 #456 到哪了?"

客户 Bob DM → session: agent:support:dm:bob_789
  "我要退货"

同一个 agent,但各自独立 session。Alice 看不到 Bob 的订单。隔离配置:per-peer

场景 2:多租户 SaaS 共享实例

为省资源让多个免费用户共享一个 OpenClaw 进程:


用户 A → session: agent:main:dm:user_a (独立 context)
用户 B → session: agent:main:dm:user_b (独立 context)
用户 C → session: agent:main:dm:user_c (独立 context)

一个进程,三套独立 context window。但风险是工具层(exec、文件读写)共享同一个 OS 用户,需要配合容器/沙箱做物理隔离。

场景 3:群聊(通常共享 session)

群聊本身是公开的,所有人看同一个 context。但同一个人的 DM 必须与群聊隔离。

场景选型指南

场景隔离需求推荐配置
个人助手(只有自己用)不需要`main`
客服 Bot(多客户 DM)**必须隔离**`per-peer`
多平台同一 Bot按平台+用户隔离`per-channel-peer`
多租户 SaaS 共享实例最严格隔离`per-account-channel-peer` + 容器
群聊通常共享默认

第 3 层:命令授权(谁能执行什么)

核心文件: src/channels/command-gating.ts, src/channels/mention-gating.ts

即使能跟 Bot 说话,执行命令还需要额外授权。这层包含三个子机制:

3a. Mention Gating(@提及门控)

群聊里最常见的问题:Bot 该不该响应每条消息?


配置项: channels.discord.guilds.<id>.mentionGating
模式行为
`required`必须 @Bot 才触发(默认群聊行为)
`optional`@Bot 触发,不 @ 也可能触发(看其他条件)
`disabled`所有消息都处理(适合专属频道)

为什么重要:没有这层,群里 50 个人聊天,Bot 会对每条消息都跑一次 LLM 推理——浪费 token 且很吵。

3b. Access Groups(访问组)

基于 Discord 角色 / Slack 用户组做分级权限:


# 示例配置
channels:
  discord:
    guilds:
      "<guild_id>":
        useAccessGroups: true
        accessGroups:
          admin:
            roles: ["Admin", "Owner"]
            commands: ["*"]              # 所有命令
          member:
            roles: ["Member"]
            commands: ["ask", "search"]  # 只能问和搜
          default:
            commands: []                 # 无权限

判定逻辑


用户发 /command
  → 查用户 Discord 角色
  → 匹配到 accessGroup
  → 检查该 group 的 commands 列表
  → 包含则放行,否则拒绝

这意味着同一个群里,Admin 可以执行 /exec/restart,普通 Member 只能问问题。对多租户平台来说,这层可以用来区分付费用户和免费用户的能力。

3c. Command Gating(命令门控)

更细粒度的单命令级控制:


某些命令标记为 "gated"
  → 执行前检查发送者是否在该命令的 allowFrom 列表
  → 不在则返回 "⛔ You don't have permission"

与第 1 层的区别

类比:第 1 层是门禁卡刷进大楼,第 3 层是你进了大楼但只能去 3 楼不能去 7 楼。

三层联合判定流程


消息到达(已通过第1/2层)
  │
  ├─ 是群聊?
  │   └─ mentionGating=required 且没 @Bot?→ 忽略
  │
  ├─ 是 /command?
  │   ├─ useAccessGroups=true?
  │   │   └─ 用户角色匹配的 group 不含此 command?→ 拒绝
  │   └─ command 有独立 gate?
  │       └─ 用户不在 allowFrom?→ 拒绝
  │
  └─ 通过 → 进入第4层(输入消毒)

关键设计:这层的拒绝是静默的——不触发 LLM 调用,不消耗 token,不产生响应。被拒的消息就像从没发过一样。这跟第 5/6 层不同(那两层是 Agent 已经在跑了,工具/命令级别拦截)。

第 4 层:输入消毒(防注入)

核心文件: src/agents/sanitize-for-prompt.ts, src/security/external-content.ts

4a. 基础消毒

sanitizeForPromptLiteral() 对所有嵌入 prompt 的用户数据:

4b. 外部内容隔离(重点防线)

对来自邮件、webhook、网页抓取等不可信来源的内容,wrapExternalContent() 做三件事:

1) 边界标记包裹


<<<EXTERNAL_UNTRUSTED_CONTENT id="a3f8c2e1">>>
[不可信内容]
<<<END_EXTERNAL_UNTRUSTED_CONTENT id="a3f8c2e1">>>

每次用随机 8 字节 hex ID,防伪造。

2) 注入模式检测(12 种模式):

3) 同形字防御

将全角 <>、CJK 括号、数学括号等 27 种 Unicode 变体归一化为 ASCII,防止绕过边界标记。

第 5 层:工具策略(Agent 能用什么工具)

核心文件: src/agents/tool-policy-pipeline.ts, src/agents/pi-tools.policy.ts

即使消息通过了前 4 层,Agent 想调用工具时还要过这关。这层决定 LLM 能"看到"哪些工具。

5a. 工具 Profile(4 级能力等级)

每个 Agent 有一个 tool profile,像游戏角色的职业等级:


minimal → coding → messaging → full
   │         │         │         │
   │         │         │         └─ 所有工具(含危险的)
   │         │         └─ + 消息发送(message、sessions_send)
   │         └─ + exec、文件读写、browser
   └─ 只有 web_search、web_fetch 等只读工具

每个工具在代码里标记了自己属于哪些 profile:


// 举例
exec        → profiles: ["coding", "messaging", "full"]
web_search  → profiles: ["minimal", "coding", "messaging", "full"]
message     → profiles: ["messaging", "full"]

minimal profile 的 Agent 想执行 exec?工具压根不会出现在可用列表里——LLM 都看不到这个工具的存在。

5b. 策略叠加(5 层覆盖,deny 优先)

工具可用性不是简单的 profile 一刀切,而是多层策略叠加:


[1] Profile 基线        → profile=coding,拿到该 profile 所有工具
        ↓
[2] Provider 覆盖       → 某些 provider(如 HTTP gateway)额外禁用危险工具
        ↓
[3] 全局 allow/deny     → config 里 tools.allow / tools.deny 列表
        ↓
[4] Agent 覆盖          → 特定 agent 的 toolPolicy
        ↓
[5] Group 覆盖          → Discord 角色等群组级覆盖
        ↓
最终可用工具列表

关键规则:deny 永远优先于 allow。任何一层说 deny,后面的层说 allow 也没用:


全局: deny exec
Agent: allow exec
结果: deny(全局 deny 不可被覆盖)

5c. 危险工具硬编码拦截

某些工具被硬编码标记为 dangerous,在特定场景自动拦截:


硬编码危险工具:
├── exec, spawn, shell          ← 命令执行
├── sessions_spawn, sessions_send  ← 跨会话操作
├── gateway                     ← 网关管理
├── fs_write, fs_delete         ← 文件系统修改
└── apply_patch                 ← 代码修改

dangerous 不等于"只能本地用",而是按接入方式的信任级别自动决定:

接入方式信任级别dangerous 工具
本地 CLI(`openclaw chat`)最高✅ 可用
本地 Gateway + token 认证✅ 可用
Tailscale 网络 + 机器身份认证✅ 可用
公网 Gateway + token 认证⚠️ 看配置(可显式 allow)
公网 Gateway 未认证❌ 硬拦截
HTTP API 匿名调用❌ 降级 minimal

典型链路(渠道消息为什么是"本地"信任级别)

渠道消息(Discord/Telegram/Signal 等)走的是拉模式,不是推模式:


你在 Discord 发消息
     ↓
Discord 服务器推送事件
     ↓
本机 OpenClaw 进程(主动连接 Discord API 拉消息)
     ↓
Gateway(127.0.0.1:18789,本地 loopback)
     ↓
Agent 处理(exec 等 dangerous 工具可用)

关键点:Gateway 只监听 127.0.0.1(本地回环),外部网络根本访问不到。OpenClaw 是主动去拿消息,不是被动接收外部请求。所以 Discord/Telegram 等渠道消息天然等效于"本地 Gateway + 认证通过"的信任级别。

这跟"外部 HTTP 直接打 Gateway 端口"完全不同:


外部 HTTP 请求 → 公网 IP:18789 → Gateway → 这才是"远程接入" → dangerous 工具默认禁用

多租户场景注意:如果用户的 OpenClaw 实例暴露了 HTTP Gateway 让外部访问,dangerous 工具会被自动禁用,除非显式配置 tools.allow: ["exec"] 覆盖。这是安全兜底——防止有人通过 API 远程执行命令。

5d. 子 Agent 工具限制(防递归炸弹)

子 Agent 的工具集比父 Agent 更严格:


父 Agent (main)
├── 可用: exec, message, sessions_spawn, gateway, memory_search...
│
└── 子 Agent (subagent)
    ├── 禁用: gateway, memory_search, whatsapp_login(永久黑名单)
    │
    └── 叶子节点(最深层子 agent)
        └── 额外禁用: subagents, sessions_spawn(防无限套娃)

如果子 Agent 能 spawn 子子 Agent,子子 Agent 再 spawn……无限递归会耗尽资源。所以最深层强制断链。

5e. 与第 6 层的区别


第 5 层:Agent 的工具列表里有没有这个工具?(编译期)
         → 没有 = LLM 根本不知道这个工具存在

第 6 层:这次具体的命令/参数允许执行吗?(运行期)
         → exec("cat /etc/passwd") 工具存在,但这条命令要审批

类比:第 5 层是"你有没有驾照",第 6 层是"你今天能不能开这条路"。

5f. 实际配置举例


main agent (profile: full)
├── exec ✅  web_search ✅  message ✅  sessions_spawn ✅

researcher agent (profile: coding)
├── exec ✅  web_search ✅  message ✅  sessions_spawn ❌

qwen agent (profile: minimal)
└── exec ❌  web_search ✅  message ❌  sessions_spawn ❌

第 6 层:Exec 审批(命令执行关卡)

核心文件: src/infra/exec-approvals.ts, src/gateway/exec-approval-manager.ts

审批流程


命令 → 白名单匹配 → 不匹配则挂起 → 多渠道通知 → 用户审批 → 执行或拒绝

审批判定逻辑


requiresExecApproval({ ask, security, analysisOk, allowlistSatisfied }) {
  return (
    ask === "always" ||
    (ask === "on-miss" &&
      security === "allowlist" &&
      (!analysisOk || !allowlistSatisfied))
  );
}

审批决定

决定效果
`allow-once`本次放行,白名单不更新
`allow-always`放行并写入白名单,以后同类命令免审批
`deny`拒绝执行
超时(120 秒)等同于 deny

Safe bins 机制

核心文件: src/infra/exec-safe-bin-policy-profiles.ts

第 7 层:秘密保护(防泄露)

日志脱敏

核心文件: src/logging/redact.ts

自动识别并遮盖:

模式示例
Token 前缀`sk-*`, `ghp_*`, `xox*`, `AIza*`, `npm_*`
环境变量赋值`API_KEY=xxx`
JSON 字段`"token": "..."`
Auth header`Bearer eyJ...`
PEM 密钥块`-----BEGIN PRIVATE KEY-----`

遮盖策略:短于 18 字符 → *;否则保留前 6 + 后 4(如 sk1234…abcd)。

环境变量隔离

核心文件: src/agents/sandbox/sanitize-env-vars.ts

沙箱/子进程启动时,自动过滤掉:

只放行:PATHHOMELANGNODE_ENV 等安全变量。

配置脱敏

核心文件: src/config/redact-snapshot.ts

Web UI 请求配置时,敏感字段(channels..authgateway.auth.)替换为 REDACTED_SENTINEL,写回时从磁盘还原真实值。

第 8 层:传输与存储安全

Gateway 认证

核心文件: src/gateway/auth.ts

认证模式说明
`none`无认证(仅 loopback)
`token`Bearer token
`password`Basic auth
`tailscale`Tailscale 机器身份
`trusted-proxy`代理头信任
`device-token`设备签名配对

TLS

核心文件: src/infra/tls/gateway.ts

文件权限


~/.openclaw/              → 0o700 (仅用户)
~/.openclaw/credentials/  → OAuth、频道 token
~/.openclaw/identity/     → Ed25519 设备密钥对 (0o600)