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
- 默认只用 ID 匹配(不可变,安全)
- 名字/用户名匹配需要显式开启
dangerouslyAllowNameMatching=true(因为改名就能冒充) "*"通配符 = 允许所有人
allowFrom 配置详解
allowFrom 是第 1 层的核心参数——白名单本体。消息进来第一件事就查 allowFrom,不在列表里的直接丢弃,连 session 都不会创建,后面 7 层全不走。
# 典型配置
channels:
discord:
allowFrom: ["384619457508540416"] # Discord 用户 ID
telegram:
allowFrom: ["@frankyoung2024"] # Telegram 用户名
allowFrom 和 dmPolicy 的关系:
| dmPolicy | allowFrom 作用 | 效果 |
|---|---|---|
| `allowlist` | 只放行列表中的人 | 最严格,生产环境推荐 |
| `pairing` | 陌生人走配对流程,通过后等效加入白名单 | 适合需要动态添加用户的场景 |
| `open` | allowFrom 被忽略,所有人都能聊 | 公开 Bot(客服等) |
| `disabled` | 谁都不行 | 完全关闭 DM |
设 allowFrom: ["*"] 等效于 dmPolicy: "open",任何人都能私聊 Bot。
配对系统
核心文件: src/pairing/pairing-challenge.ts
- 陌生人私聊 → 发配对码 → 用户在可信界面输入 → 配对成功才放行
- 待审批请求最多 3 个,1 小时过期
第 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 之间的隔离是硬性的:
- 每个 agent session 有独立的 context window、memory 文件、tool 权限
- Agent A 看不到 Agent B 的任何对话
唯一的跨会话通道是显式的、受控的:
sessions_send— 主动发消息给另一个 sessionsessions_history— 读取另一个 session 的历史(需要权限)subagent_announce— 子 agent 完成后推送结果给父 session
这些都是可审计的跨界操作,不是隐式的数据共享。
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 层:能不能跟 Bot 说话(身份验证)
- 第 3 层:能执行哪些操作(权限控制)
类比:第 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 的用户数据:
- 剥离 Unicode 控制字符(Cc、Cf 类)
- 剥离行/段分隔符(Zl、Zp)
- 防止换行注入改变 prompt 结构
4b. 外部内容隔离(重点防线)
对来自邮件、webhook、网页抓取等不可信来源的内容,wrapExternalContent() 做三件事:
1) 边界标记包裹:
<<<EXTERNAL_UNTRUSTED_CONTENT id="a3f8c2e1">>>
[不可信内容]
<<<END_EXTERNAL_UNTRUSTED_CONTENT id="a3f8c2e1">>>
每次用随机 8 字节 hex ID,防伪造。
2) 注入模式检测(12 种模式):
"ignore all previous instructions""you are now a/an...""/" "[System Message]""rm -rf"/"delete all...""elevated=true"- 等等
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
jq、grep、cat、sort、head等纯 stdin 工具可免审批- 但必须来自受信目录(默认只信
/bin、/usr/bin) - 每个 bin 有 profile 限制(允许的 flag、最大位置参数数等)
- 解释器类(python、node、ruby)默认拒绝
第 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
沙箱/子进程启动时,自动过滤掉:
- 所有
_API_KEY、_TOKEN、_PASSWORD、_SECRET结尾的变量 - 特定变量:
TELEGRAM_BOT_TOKEN、DISCORD_BOT_TOKEN、GH_TOKEN等
只放行:PATH、HOME、LANG、NODE_ENV 等安全变量。
配置脱敏
核心文件: src/config/redact-snapshot.ts
Web UI 请求配置时,敏感字段(channels..auth、gateway.auth.)替换为 REDACTED_SENTINEL,写回时从磁盘还原真实值。
第 8 层:传输与存储安全
Gateway 认证
核心文件: src/gateway/auth.ts
| 认证模式 | 说明 |
|---|---|
| `none` | 无认证(仅 loopback) |
| `token` | Bearer token |
| `password` | Basic auth |
| `tailscale` | Tailscale 机器身份 |
| `trusted-proxy` | 代理头信任 |
| `device-token` | 设备签名配对 |
- Token 比较用
crypto.timingSafeEqual(src/security/secret-equal.ts),防时序攻击 - 失败认证有 per-IP 速率限制
TLS
核心文件: src/infra/tls/gateway.ts
- 支持自签证书自动生成(RSA 2048)
- 私钥文件
0o600权限
文件权限
~/.openclaw/ → 0o700 (仅用户)
~/.openclaw/credentials/ → OAuth、频道 token
~/.openclaw/identity/ → Ed25519 设备密钥对 (0o600)