发送者白名单(第零层)
每条 WebSocket 消息在进入下面任意一层之前,都先经过 trusted sender 白名单。这一页讲清三个白名单源、强制 strict mode 启动 行为,以及让运维可以在不重启的情况下把非授信用户加入特定 聊天的 authorize-mention OOB 流程。
第零层与其他层的关系见 权限矩阵。
白名单如何拼装
ringclaw start 启动后,WebSocket monitor 与消息 handler 都进入 strict sender 模式:只有 trusted sender 白名单上的用户 ID 才能驱动 AI agent。该白名单是以下 三者的并集:
- Private App owner ID(配置了 Private App 时自动注入)。
ringcentral.source_user_ids中所有条目(启动时解析为数字 user ID)。- 目标聊天的
ringcentral.chat_user_allow[<chatID>](启动时 按source_user_ids同样的方式解析)。这是按群分层叠加的 例外而不是全局放宽——chat_user_allow只在列出的群里允许 列出的用户。
如果三者全空,bot 在启动时打 ERROR 并 丢弃所有 收到的消息, 直到运维加入至少一个 trusted sender。这避免了"任意进入允许 聊天的人都可以驱动 AI agent"这种隐患。
ringcentral:
source_user_ids:
- "+15551234567" # 电话号码,启动时解析
- alice@example.com # 邮箱,通过 Private App directory 解析
- "987654321" # 直接的数字 extensionId / user IDTIP
邮箱与电话号码条目需要带 ReadAccounts 权限的 Private App 才能解析为数字 ID。没有 Private App 时请直接列数字 extensionId。
chat_user_allow —— 按群例外层
ringcentral.chat_user_allow 是叠加在 source_user_ids 之上的 按群白名单:
{
"ringcentral": {
"chat_user_allow": {
"chat-engineering-7": ["alice@example.com", "3061708020"],
"chat-design-9": ["bob@example.com"]
}
}
}标识符可以是数字 extension ID、邮箱或 E.164 电话号码(启动时 通过 Private App directory 解析为数字 ID)。运维可以手工预置, 也可以让 authorize-mention OOB 流程 在批准时自动写入。
关键不变量:chat_user_allow 只放宽列出聊天的发送者白名单。 它不会解锁特权第一层命令(/cwd、/cron、/new、/reload、 /full-access、总结自然语言触发)—— 这些仍要求 Private-App-owner 身份。详见 命令授权。
Authorize-mention OOB 流程
RingClaw 在与 /full-access、跨聊天 OOB challenge 共用一套
安全公告(v0.4.2 → v0.4.3)
v0.4.3 之前任何变成"trusted"的发件人——无论是通过 source_user_ids、chat_user_allow 还是 v0.4.1 OOB 审批—— 都驱动与 bot 操作员相同的 agent 后端,能在 agent 工具调用通道 里发起文件系统(List、Read、Write)、终端(Bash)、 外部 HTTP 调用。
v0.4.3(本版本)—— 针对 fs/* + terminal/* 的双层 fail-closed 非-owner 隔离:
- "Owner" 现在严格只是
source_user_ids集合(加上解析出 的 Private App 机主)。chat_user_allow用户、v0.4.0 OOB 审批通过的用户都被视为 non-owner,按下述受限上限运行。 - 第一层(协议): 创建非-owner 的 ACP 会话之后立刻发送
session/set_mode <restricted>。modeID 走每家 agent 一张表:droid → spec,claude → plan,gemini → plan,qwen → plan,cursor-agent → plan。未知 agent 走启发式: 在availableModes里找名称含plan/spec/read/safe的 mode。 - 第二层(客户端 fail-closed gate): ringclaw 拒绝来自 非-owner 会话的任何
fs/read_text_file/fs/write_text_file/terminal/create/terminal/output/terminal/wait_for_exit/terminal/kill/terminal/releaseJSON-RPC 请求,无视 agent 本身对 mode 的执行情况。session/request_permission也会被 deny(agent 收到的是kind=deny选项或cancelled结果)。 - 找不到只读 mode = fail-closed。 当 agent 没有任何只读 mode 且运维也没配 override 时,非-owner 消息不会进入 agent;用户拿到一段拒绝文案;审计日志打
restricted_mode_unsupported_no_mode。 chat_user_allow重启后保留(v0.4.4+)。 v0.4.2 启动时 强制清空的措施已经移除——v0.4.3 的非-owner ceiling 已经在 ACP 层把这些会话锁住。被列入的用户在 Layer 0 放行,然后按上面的 fail-closed ceiling 运行。
第一层已知局限(best-effort): 部分上游 agent 不会强制执行 只读 mode(qwen-code#1806 set_mode 返回成功但不强制; gemini-cli#22191 plan mode 在 ACP 路径有已知 bug)。fs/* + terminal/* 的真正安全边界由第二层提供。WebFetch / WebSearch / 内置 HTTP 工具 / MCP 自定义工具 由 agent 进程 本身发起,ringclaw 看不到对应的 JSON-RPC 请求,因此第二层无法 覆盖——这部分仍只能依赖第一层 best-effort。v0.5.0 计划用 OS 级别 sandbox 把这部分也封住。
运维 override: 每家 agent 的默认 modeID 可以通过 config.json 中的 agents.<name>.restricted_mode_id 覆盖。 override 必须在 agent 自己的 availableModes 列表里,否则 内置选择仍然生效。
challenge / 主机审批基础设施之上叠加了一条独立 OOB 入口,让 运维可以按群临时把非授信用户加入信任范围而无需重启或手工 编辑 config.json。开关字段是 ringcentral.allow_group_mention_authorize:
- 未设置(v0.4.2 起的默认):功能关闭。非授信群聊
@bot静默丢弃——与 v0.4.0 行为一致。 true:功能开启。非授信群聊@bot会在 owner 私聊里 弹出/approval审批提示。要求运行时存在 Private App + 已 解析的 owner 私聊;缺失时启动时打 ERROR 日志并禁用功能。 ringclaw 还会在启动时打一条 WARN 提醒运维:被批准的用户当 前会获得 agent 完整能力。false:功能关闭,等同未设置。
触发条件很窄:用户不在全局 source_user_ids 白名单,也 不在目标聊天的 chat_user_allow 条目中,并且在允许的群聊里 发出真正的 @bot 消息。同一用户的纯文本消息、或非允许聊天里 的 @bot 仍然按旧逻辑丢弃。
关键不变量
- 原始消息丢弃。 触发 challenge 的那条
@bot不会在 批准后重放。用户必须再@bot一次才能真正驱动 AI。这避免 了"先写好的 prompt 在运维批准之后被无意触发"。 - 仅按群作用域。 授权落在
chat_user_allow[<chatID>],绝 不会写入全局source_user_ids。在群 A 批准的用户不会因此在 群 B 也被信任。 - 特权第一层命令仍未解锁。
chat_user_allow只放宽第零 层。非 owner 的特权第一层命令仍要求 Private-App-owner 身 份。详见 命令授权。
第二层(agent 工具调用)—— 非-owner 上限(v0.4.3+)
被列入的用户可以像任何 trusted sender 一样驱动 AI agent,但 v0.4.3 在他们的会话上加了一道 fail-closed 上限:
fs/read_text_file、fs/write_text_file、terminal/create/output/wait_for_exit/kill/release、session/request_permission在 ringclaw 客户端层就被 deny。agent 拿到的是code=-32001("非-owner 发件人调用被拒")。- 会话同时被请求切换到只读 mode(droid 走
spec,其余走plan)作为纵深防御。当 agent 不支持任何合适 mode 时, 消息会直接被拒,不进入 agent。 - 文字回复与 RingCentral
ACTION:MESSAGE / TASK / NOTE / EVENT仍可用(后者走 RingCentral REST API,不走 ACP)。
第二层覆盖不到的部分: WebFetch、WebSearch、MCP 自定义 工具。这些是 agent 进程内部派发的,ringclaw 看不到。v0.5.0 计 划通过 OS 级 sandbox 把这部分也封住。在那之前,把 chat_user_allow 用户视为"可以让 agent 读公网、可以让 agent 说话",但不再是"可以 shell 进你的主机"。
- Pending dedupe。 同一
(chatID, userID)在 challenge TTL 内同时只能存在一个 pending challenge。批准 / 拒绝 / 过期 / prompt 发送失败任一收尾路径都会释放这把锁。 - 24 小时冷却(v0.4.1+)。 challenge 走完拒绝或过期路径 后,同一
(chatID, userID)进入 24 小时静默期:期间该用户 在该群再次@bot会被直接丢弃,不再向 owner 私聊投递新的 审批提示。这能避免一个吵闹或恶意的非授信用户反复@bot把 owner 私聊灌爆。批准路径不写冷却(用户已通过chat_user_allow信任),瞬时错误(如 owner 私聊发送失败) 也不写冷却,让运维下次有机会再处理。冷却状态仅存在内存 中,进程重启会清空——这是可以接受的,因为重启本就是运维主 动操作。 - 持久化是 best-effort。 批准时同步更新 monitor + handler 的内存白名单,再触发
config.jsonSave。Save 失败时打ERROR authorize-mention: persist failed,本进程内仍然信任, 但下次重启会丢失——重视持久化的运维请关注该日志行。 - 持久化优先存邮箱。 目录解析能拿到邮箱时存邮箱;否则存 数字 extension ID 并打
WARN authorize-mention: no email available。手工编辑时也可使用source_user_ids接受的三种 形式(数字 / 邮箱 / E.164 电话),下次启动会自动解析。 - 不引入新审批命令。 主机 CLI 仍然是
ringclaw approval <id>/ringclaw approval deny <id>,同 一条命令统一审批 authorize-mention、/full-access与跨聊天 OOB challenge。challenge 的intent字段在审计日志中区分类 型。详见 审批 CLI。 - Owner self-challenge 守卫。 当
post.CreatorID与 Private App owner 的 ID 相同时,handler 拒绝发起 authorize-mention challenge。Monitor 的第零层已经允许 owner 通过;走到这里意味着出现 bug 或恶意直接调用,fail-closed 防 止 owner 被路由到一条"用户 X 申请授权"的 prompt(其实 X 就 是自己)。
失败模式
| 条件 | 行为 |
|---|---|
allow_group_mention_authorize 未设置(v0.4.2 起的默认) | 功能关闭,非授信 @bot 静默丢弃(v0.4.0 基线)。 |
allow_group_mention_authorize: false | 显式关闭,行为与未设置一致。 |
allow_group_mention_authorize: true | 显式开启。ringclaw 启动时打一条 INFO 提醒:被批准用户运行在 v0.4.3+ 非-owner ceiling 下。 |
| Private App 未配置(owner 私聊不可解析) | 启动时禁用功能并打 ERROR 日志;非授信 @bot 静默丢弃。 |
chat_user_allow 条目解析为零数字 ID | v0.4.4:打 WARN chat_user_allow entry resolved to zero numeric IDs 并附上 raw identifiers。最常见的原因是 chat ID 字段填的是 team 显示名而不是数字 chat ID;其次是 identifier 不在 directory 里、Private App 没有 ReadAccounts 权限。 |
| 非-owner 命中没有只读 mode 的 agent | v0.4.3 fail-closed:消息不进入 agent;用户收到一段拒绝文案;审计日志打 restricted_mode_unsupported_no_mode。 |
非-owner + agent 拒绝 session/set_mode | v0.4.3 fail-closed:同上;(agentCmd, modeID) 被缓存,后续尝试跳过该 RPC。 |
| 运行时 owner 私聊尚未解析 | 命中竞争窗口的那一条消息丢弃并打 WARN authorize-mention: OOB or owner DM unconfigured; dropping;私聊解析完成后后续消息正常。 |
| 运维拒绝 challenge | 释放 pending 锁;owner 私聊收到通知。(chat, user) 进入 24 小时冷却——期间再 @bot 会被静默丢弃。 |
| Challenge 过期(5 分钟 TTL) | 释放 pending 锁;owner 私聊收到过期通知。同样进入 24 小时冷却。 |
| Owner 私聊发送失败(RC 瞬时错误) | challenge 自动 deny;释放 pending 锁。不写冷却,下次 @bot 重新尝试。 |
| 持久化 callback 失败(如配置文件写错) | 内存中的授权仍然有效;打 ERROR authorize-mention: persist failed。重启后用户重新被锁。 |
不开启 OOB 直接预置授权
想在不启用 OOB 流程的情况下信任已知用户,可以手工编辑 chat_user_allow:
{
"ringcentral": {
"allow_group_mention_authorize": false, // OOB 关闭
"chat_user_allow": {
"800123456": ["alice@example.com"]
}
}
}这只在 chat 800123456 中信任 Alice,不会触发任何 challenge。 两个字段互相独立。map 的 key 必须是 RC 数字 chat / group ID (参考 如何获取 chat ID)——填 team 显示名 不会匹配任何消息。
群成员 OOB 审批的完整启用步骤
非-owner 群成员需要在某个群里驱动 agent 时,按下面这份清单走。 被批准的用户运行在 v0.4.3+ 非-owner ceiling 下:文字回复 + RC ACTION:MESSAGE / TASK / NOTE / EVENT 仍可用;fs/*、 terminal/*、session/request_permission 在 JSON-RPC 层就被 拒绝。
前置条件(4 个全部必须)
- 配好 Private App。 OOB 需要读 owner 私聊。bot client 单独看不到自己的 DM,所以 Private App 用运维身份去读。少了
ringcentral.private_app这一段,OOB 在启动时会被禁用并打ERROR日志。 - owner 至少和 bot 私聊过一次。 ringclaw 由那段对话解析
ownerDMChatID。解析失败会看到ERROR allow_group_mention_authorize requires Private App + resolved owner DM; feature disabled。 chat_ids里包含目标群的数字 chat ID。 不在chat_ids名单里的群,消息在更早的 step 就被丢弃,根本走不到 OOB。source_user_ids非空(Private App owner 启动时会自动 注入),否则 strict mode 会把所有消息都丢掉。
最小 config
{
"ringcentral": {
"bot": {
"server_url": "https://platform.ringcentral.com",
"token": "<bot oauth token>"
},
"private_app": {
"server_url": "https://platform.ringcentral.com",
"client_id": "...",
"client_secret": "...",
"jwt": "..."
},
"chat_ids": ["800123456"],
"source_user_ids": ["owner@yourcompany.com"],
"group_mention_only": true,
"allow_group_mention_authorize": true,
"chat_user_allow": {}
},
"agents": {
"droid": {
"type": "acp",
"command": "droid",
"restricted_mode_id": "spec"
}
}
}chat_user_allow 留空就行——OOB 审批通过会自动写入。也可以 预先手填(见上面预置授权),但 key 必须是数字 chat ID 不是 team 显示名。
启动期望日志
一切配齐时,按顺序应该看到:
INFO source_user_ids resolved,列出 owner ID。INFO authorize-mention OOB enabled — approved users run under v0.4.3+ non-owner ceiling …INFO authorize-mention OOB flow active ownerDMChatID=…
看到 ERROR allow_group_mention_authorize requires Private App + resolved owner DM:前置条件 #1 或 #2 没满足。
看到 WARN chat_user_allow entry resolved to zero numeric IDs: 某个 chat key 错了(八成是写了 team 名而不是数字 chat ID),或 identifier 不在 directory 里。
审批流
- 群成员
@bot 你好在配置好的群里发言。 - ringclaw 把原消息丢弃(不会在批准后重放),向 owner 私聊 投递
/approval <id>提示。 - 运维在 bot 所在主机执行
ringclaw approval <id>。 - ringclaw 把被批准的用户写入
chat_user_allow[<chatID>](v0.4.4+:重启后保留)以及运行时的 Monitor + Handler 白名单。 - 群成员再
@bot一次——这次消息会以非-owner ceiling 进入 agent。
同一对 (chat, user) 在 deny / 过期后会被静默 24 小时(避免反复 打扰 owner 私聊)。详见 关键不变量。
如何获取 chat ID
chat_ids 与 chat_user_allow 的 key 都需要 RC 数字 chat / group ID(通常是 9–10 位数字字符串如 "800123456")。填 team 显示名不会匹配任何消息。三种获取方法:
- RingCentral 客户端 URL。 在 RingCentral 网页 / 桌面客户端 打开那个聊天,URL 末尾
/r/<chatID>中的数字就是 chat ID。 - ringclaw debug 日志。 设置
RINGCLAW_LOG_LEVEL=debug,群里 随便发条消息,看 monitor 行ignoring message from non-allowed chat chatID=…,这里的chatID就是。 - REST
glip/groups。 用 Private App 凭据调GET /restapi/v1.0/glip/groups?recordCount=250,找到name匹配的项,id字段就是 chat ID。
审计日志条目
| 事件 | 日志行 | 用途 |
|---|---|---|
| 启动时 sender 白名单为空 | sender allowlist is empty: ... | strict mode 因为没有 source_user_ids 也没有 Private App owner 而退化为 deny-all 的标准信号。 |
| Authorize-mention 路由(monitor) | INFO authorize-mention: routing non-trusted group mention(含 chatID、userID) | 确认 WebSocket monitor 把非授信 @bot 交给 OOB 流程而非丢弃。 |
| Authorize-mention challenge 发起 | INFO oob: challenge issued(intent 以 authorize user … in chat … 开头) | 与 /full-access、跨聊天 OOB 共用同一行;intent 字段区分类型。 |
| 授权完成 | INFO authorize-mention: granted(含 challengeID、chatID、userID、identifier) | 在 applyAuthorize 更新内存白名单并持久化后触发。 |
| 拒绝 / 过期 | INFO authorize-mention: denied 或 INFO authorize-mention: challenge expired(含 cooldown=24h) | granted 行的对偶。pending dedupe 锁释放,并写入 24 小时静默窗口。 |
| 命中冷却 | DEBUG authorize-mention: in cooldown after recent deny/expire, dropping | (chat, user) 在 24 小时静默期内的再次 @bot 会被静默丢弃,不再打扰 owner。 |
| 持久化失败 | ERROR authorize-mention: persist failed(含 error) | 内存授权成功但写 config.json 失败——本进程内仍生效,重启会丢。 |
| Prompt 发送失败 | ERROR authorize-mention: post prompt failed(含 error) | Owner 私聊发送失败;challenge 自动 deny,pending 锁释放,用户可重试。 |
| 没有可用邮箱 | WARN authorize-mention: no email available, persisting numeric ID | 目录解析无邮箱;改存数字 extension ID(仍按群作用域)。 |
| v0.4.3:非-owner 应用受限 mode | WARN acp restricted-mode event(event=restricted_mode_applied、mode_id、mode_source、conversation、sender_id) | 第一层成功:agent 接受了非-owner 会话的只读 mode。 |
| v0.4.3:受限 mode 不可用(无候选) | WARN acp restricted-mode event(event=restricted_mode_unsupported_no_mode、available_modes) | 第一层 fail-closed:内置表与启发式都没匹配。非-owner 消息被拒。 |
v0.4.3:受限 mode 不可用(set_mode 被拒) | WARN acp restricted-mode event(event=restricted_mode_unsupported、error="Method not found") | 第一层 fail-closed:agent 拒绝调用。被缓存,后续跳过。 |
| v0.4.3:第二层工具调用被拒 | WARN acp non-owner tool call denied(event=tool_call_denied、method、session、reason) | 客户端 fail-closed 拒绝 fs/* / terminal/* / session/request_permission,按 (session, method) 去重。 |
| v0.4.4:chat_user_allow 加载 | INFO chat_user_allow resolved (v0.4.3+ non-owner ceiling enforced for these users)(chats、users) | 从 config.json 读到的按群例外,已推入 Monitor + Handler,替代 v0.4.2 的 force-clear。 |
| v0.4.4:chat_user_allow 条目解析为零 | WARN chat_user_allow entry resolved to zero numeric IDs(chatID、rawIdentifiers) | 某个 chat key(或其 identifier)解析失败。最常见原因是 key 是 team 显示名而非数字 chat ID。 |