Skip to content

feat(read-isolation): 隔离 bot 附件按 appId 分桶可读自己上传,并补齐敏感数据 deny#387

Open
xu4wang wants to merge 4 commits into
deepcoldy:masterfrom
xu4wang:pr/read-isolation-attachments
Open

feat(read-isolation): 隔离 bot 附件按 appId 分桶可读自己上传,并补齐敏感数据 deny#387
xu4wang wants to merge 4 commits into
deepcoldy:masterfrom
xu4wang:pr/read-isolation-attachments

Conversation

@xu4wang

@xu4wang xu4wang commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

改了什么

飞书上传的附件改按 appId 分桶data/attachments/<appId>/<messageId>/(原来是扁平的 data/attachments/<messageId>/)。读隔离 Seatbelt profile 放行该 bot 自己的桶(carve-out)+ traverse shim,隔离 bot 从此能读用户在群里上传的文件;attachments/ 根目录仍整树 deny(盖住兄弟桶 + 历史扁平存量),ls 枚举也挡。

同轮盘点 ~/.botmux 里其它 per-bot / 跨 bot 敏感数据,补齐一批 deny(均核实 CLI 侧无合法读取):

  • data/queues/(所有 bot 的完整入站消息内容)、data/schedules.jsondata/read-isolation/
  • ~/.botmux/feishu-session.json(网页登录态,可开新 bot)
  • ~/.botmux/.dashboard-secret(loopback-HMAC 签名密钥)+ .dashboard-token(bearer)
  • 文件名 regex 整类:data/identities-<appId>.json(对话者姓名 PII)、遗留 data/.send-cred-<appId>

downloadResources 对 path-unsafe 的 appId 软失败(catch → 空附件),不再让畸形 appId 把整条消息文本一起丢。

为什么

  • 附件:路径里没有 spawn 时可知的 key,而 Seatbelt profile 是 CLI 启动时一次性静态生成的,无法按消息动态开洞 → 原来只能整树 deny,隔离 bot 连自己收到的文件都 EPERM。按 appId 分桶后,不可变的 appId 成了静态 carve-out 锚点,与 bots/.lark-cli-bots/ 完全同一范式(不枚举兄弟、新增 bot 无需冷重启)。
  • .dashboard-secret:它签 loopback-HMAC,门禁 /__cli/rotate 和 daemon-IPC 的 write-link 路由(dashboard-ipc-server.ts 注释本就假设沙箱挡住它以阻止 sandboxed worker 铸造可写终端 token)。能触达它的是 canTalk 放行的所有人——oncall 群成员 / 团队群真人 / allowedChatGroup / 开放态,均非 owner,正是读隔离威胁模型里的「半可信对话者」→ 属跨 bot 提权,必须 deny。.dashboard-port 是纯端口号无凭证价值,留可读。

影响面

  • getAttachmentsDir 是共用路径(core/session-manager),所有 CLI × macOS/Linux 两平台生效;附件路径经 prompt 绝对路径传给 CLI,CLI 侧无耦合。
  • 非隔离 bot 仅落盘位置变化(扁平 → 分桶),无行为改变;历史存量附件留在旧布局(旧 prompt 引用的路径本就 deny)。
  • 新增 deny 只作用于隔离 bot 的 Seatbelt profile,非隔离 bot 与 daemon 无变化;downloadResources 软失败对合法 appId 是 no-op。

验证

  • pnpm build 绿;test/read-isolation.test.ts / download-resources-needlogin / claude-read-isolation / codex-read-isolation 全绿。
  • 真机 sandbox-exec 14/14:own 附件桶可读、兄弟桶 + 旧扁平布局 + queues/ + 各类凭证 EPERM、.dashboard-secret DENY / .dashboard-port ALLOW、own sessions-<appId>.json 不被新 regex 误伤、ls attachments/ 枚举仍被挡。
  • 已部署 live daemon 并在飞书实测:隔离 bot 读取新上传附件成功。

xu4wang and others added 2 commits July 6, 2026 23:04
问题:飞书上传的附件存 data/attachments/<messageId>/,路径无 spawn 时可知
的 key,Seatbelt profile 又是 spawn 时静态生成 → 只能整树 deny,隔离 bot
连自己收到的文件都 EPERM。

修复:附件改按 appId 分桶 data/attachments/<appId>/<messageId>/
(getAttachmentsDir),attachments/ 整目录保持 wholesale deny(盖住兄弟桶
+历史扁平存量),carve-out 只放行自己的桶 + traverse shim——与 bots/、
.lark-cli-bots/ 同一范式,不枚举兄弟 appId,新增 bot 无需冷重启。

同轮数据盘点补 deny(均核实 CLI 侧无读取):
- data/queues/(所有 bot 的完整入站消息内容)
- data/schedules.json、data/read-isolation/
- feishu-session.json(网页登录态,可开新 bot)、.dashboard-token
- regex 整类:data/identities-<appId>.json(对话者姓名 PII,own 也不留)、
  遗留 data/.send-cred-<appId>(旧版发送凭证,存量已删)

影响面:getAttachmentsDir 是共用路径(core/session-manager),所有 CLI、
两平台生效;附件路径经 prompt 绝对路径传递,CLI 侧无耦合;非隔离 bot 仅
落盘位置变化;历史存量附件留在旧布局(旧 prompt 引用的路径本来就 deny)。

验证:pnpm build 绿;vitest 相关文件 37/37 绿,全量 7221 过(唯一失败
sandbox.test.ts 在 clean master 同样挂,与本次无关);真机 sandbox-exec
12/12(own 桶可读/兄弟桶 EPERM/枚举仍挡/own sessions 不误伤);已部署
live daemon 并在飞书实测隔离 bot 读取新上传附件成功。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ppId 软失败

codex review(重点非隔离 bot 影响面)出的两条,承接上一 commit:

1) [blocker] deny ~/.botmux/.dashboard-secret —— 之前只挡了 .dashboard-token,
   漏了签名密钥 .dashboard-secret。它签 loopback-HMAC,门禁 /__cli/rotate 和
   daemon-IPC 的 write-link 路由;dashboard-ipc-server.ts 的注释本就假设「沙箱
   挡住 secret」以阻止 sandboxed worker 给不属于自己的会话铸造可写终端 token
   ——这是半可信对话者(oncall 群成员/团队群真人/allowedChatGroup/开放态,均非
   owner,见 canTalk)驱动隔离 agent 即可触达的跨 bot 提权。deny 之后隔离 agent
   仍可正常 send/list/status(走 relay/send-cred,不签 HMAC);只有 owner 管理命令
   dashboard/term-link 会失效,本就不该由半可信 agent 执行。.dashboard-port 是
   纯端口号无凭证价值,留可读。

2) [minor] downloadResources 对 path-unsafe appId 软失败:getAttachmentsDir 内
   assertSafeAppId 现在会抛(仅手编 bots.json 的畸形 appId 触发,真实 cli_xxx 恒
   通过)。之前无校验、下载不会因 appId 抛;若直接穿到 event-dispatcher 会连整条
   消息的文本一起丢。改为 catch→记 warn→返回空附件(与下载失败同 shape),文本照常
   处理。

未修 codex 第 2 条(存活旧 pane 用旧 profile):部署 runbook 的
`suspend --isolated` 已是有效缓解(本次部署即用),隔离 bot 暂无外部用户,结构性
兜底(marker 存 profile hash)留待规则迭代变频/上外部用户时再做。

影响面:deny 仅作用于隔离 bot 的 Seatbelt profile,非隔离 bot 与 Linux daemon
无变化;downloadResources 软失败对合法 appId 是 no-op。

验证:pnpm build 绿;vitest 相关文件 32/32 绿(+软失败用例 +secret/port 断言),
全量 7222 过(唯一失败 sandbox.test.ts 在 clean master 同样挂,与本次无关);真机
sandbox-exec 14/14(secret DENY / port ALLOW / own 附件桶可读 / 兄弟桶+queues+
凭证 EPERM / 枚举仍挡);已部署 live daemon + suspend --isolated 冷启动应用新规则。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xu4wang xu4wang requested a review from deepcoldy as a code owner July 6, 2026 16:09
read-isolation.ts 把共享 schedules.json 加进读 deny,但 schedule-store 是
read-modify-write:sandbox 里读被 deny → load() 得空 map → save() 用只含新任务的
map 覆盖回写,静默抹掉其它所有 bot 的定时任务(read-modify-write 退化成 write-only)。
可达路径是正常业务(owner 让隔离 bot「明天提醒我」→ agent 跑 botmux schedule add)。

本次先放开该 deny(接受"隔离 bot 能读到别人定时提示词"的轻微泄露)解掉数据丢失 blocker。
更稳的长期解是让 schedule-store.load() 区分 ENOENT/EPERM、读失败 fail-closed 拒绝 save
——那时 deny 可保留又不丢数据,留待后续重构。加注释防回归;测试断言相应翻转。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whiteboards/ 之前整树 deny 且无 per-appId carve-out → 隔离 bot 连自己的白板都读不到
(list 返空 / read 报错),且 update 在读失败后再写会 clobber 目标板(和 schedules.json
同类的 read-modify-write 陷阱)。

按 owner 决定:白板是共享内容、本就该让协作 bot 看到,直接放开该 deny。注意白板 store
当前是全局的(无 per-chat 隔离),所以隔离 bot 能 list/read 到其它 chat 的板——已知并接受
的 tradeoff,后续可像附件那样按 chat/appId 分桶再收紧。放开读也顺带消除了上面的 clobber。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant