Skip to content

fix(auth): 隔离每轮发送人的 CLI 身份 - #1796

Open
Hyperzsb wants to merge 1 commit into
deepcoldy:masterfrom
Hyperzsb:fix/per-sender-cli-auth-isolation
Open

Hyperzsb wants to merge 1 commit into
deepcoldy:masterfrom
Hyperzsb:fix/per-sender-cli-auth-isolation

Conversation

@Hyperzsb

@Hyperzsb Hyperzsb commented Oct 10, 2026 •

Copy link
Copy Markdown

背景

triggerUserAuth 的目标是让同一 Botmux 会话按每轮消息发送人切换 CLI 身份。原有路径中,lark-cli 的首次配置可能复用宿主机应用配置,larkdev 也可能读取宿主 ~/.larkdev/auth 或 ByteCloud 环境变量,导致应用和登录态越过发送人边界。

本修复沿用现有的 per-sender HOME、turn wrapper 和自动授权架构,不引入新的 session 身份模型;重点收紧 lark-cli 应用/登录态、bytedcli JWT 与 larkdev ByteCloud 身份的边界。

改动

  • lark-cli
    • 每个发送人使用独立的 lark-cli-home,首次使用时从空目录执行原生 config init --force-init。
    • 明确不传 --new,授权页面仍允许选择已有应用,也可由用户按需新建;成功后自动复用个人应用和登录态。
    • 正常任务缺授权时自动触发配置/登录流程并回传链接,/login 仅保留兼容入口。
    • 对旧版本复制共享 issuer 产生、且没有当前 marker 的 HOME 做一次性清理重建,避免继续继承宿主配置。
  • bytedcli / larkdev
    • 继续按发送人生成并发布 ByteCloud JWT;为 larkdev 补齐各 control plane 的用户 JWT,BOE 按 SDK 语义复用 CN。
    • larkdev 每次调用使用空的临时 auth 目录,清理宿主 JWT、AK/SK、BYTECLOUD_AUTH_AS 和 ByteClaw 标记;wrapper 退出时清理该临时目录。
  • 每轮身份边界
    • 身份文件绑定 turn,发送人切换或 turn mismatch 时 fail closed;撤销上一轮凭据后再发布当前发送人身份。
    • fs-policy 无条件密封 ~/.cache/botmux/session-tmp,只对当前会话重开其私有 scratch;未启用 triggerUserAuth 的沙箱也不能读取其他会话留下的凭据快照。
    • scratch sandbox 在 Linux/macOS 上同样密封共享 session-tmp,并以更深的可写 carve 仅重开当前会话目录。
    • 委托与定时任务支持 larkdev,同时保留旧配置兼容:省略 tools 时仍只启用 lark-cli/bytedcli,不会自动扩大权限。
  • 可诊断性与收敛
    • config init 30 秒内未产生 URL 时记录去除终端控制符后的输出尾部与当前 TUI 阶段。
    • lark-cli 快照排除 .botmux-login-challenge,并明确注释每轮 auth status --verify 网络校验的原因。
    • 个人凭据目录初始化失败会记录独立错误,不再与普通未授权状态混淆。

界面示意:

按发送人隔离的 CLI
[x] lark-cli   [x] bytedcli   [ ] larkdev

影响面

  • triggerUserAuth 的应用/用户授权、JWT 发布和 wrapper 切换只影响显式启用该能力的 Bot;larkdev 仍需显式选择。
  • 共享 session-tmp 的 deny 对所有 fs-policy/scratch 沙箱会话无条件生效,当前会话通过更深路径重开自己的目录;非沙箱会话不受影响。
  • PTY/tmux、普通话题、恢复会话及委托/定时路径共用同一 turn wrapper;其它 CLI 适配器不变。
  • 未开启文件 sandbox 时,wrapper 能保证正常命令路径按人切换并阻断宿主凭据回退,但同一 Unix 用户下不宣称 OS 级文件隔离。

兼容与升级说明

  • 旧的无 marker lark-cli HOME 会一次性清理;已经由当前逻辑成功初始化的个人 HOME 会直接复用,不会每次重新登录或强制创建应用。
  • lark-cli 的 bot-app OAuth fallback 已移除。存量用户第一次使用新版逻辑时,需要完成“个人应用选择 + 用户授权”两个步骤。
  • 当前实现按本机 lark-cli 1.0.97 的 config init --force-init TUI/参数契约验证;没有使用会强制创建新应用的 --new。
  • PR 提交已 rebase 到 origin/master 9b8bb024f,author/committer 均使用 GitHub 身份 Hyperzsb。

验证

  • bun run build
    • 通过,runtime build id:b05370dacca9
  • 身份隔离与沙箱定向测试(15 个文件)
    • 15 passed
    • 806 passed, 2 skipped
  • 真实 bwrap 回归
    • 当前会话的 lark-cli-home 可读写。
    • 同一 dataDir 下兄弟会话的凭据快照不可读、不可写。
  • scratch 编译器回归
    • Linux:shared deny → 当前会话 rw bind → shared root remount-ro。
    • macOS:shared read/write deny 后仅重开当前会话;不会把 deny 扩大为整个 ~/.cache。
  • bun run test(原始提交的全量基线)
    • Test Files:12 failed | 1622 passed | 4 skipped
    • Tests:18 failed | 29650 passed | 95 skipped
    • 全量未宣称全绿;剩余失败包含容器禁止 bwrap 挂载 /proc、root 下 chmod 无法模拟 EACCES、watcher EMFILE、运行时 /proc/comm=MainThread 及既有进程/adopt 路径断言。本次修改覆盖的定向测试均通过。
  • live 验证
    • bun run daemon:restart
    • supervisor、daemon、dashboard 均 online,restart count 为 0
    • 默认 tmux client/server 均为 3.8

@deepcoldy

Copy link
Copy Markdown
Owner

@Hyperzsb 你好,已为这个 PR 创建自动评审群,请点击下面的链接加入(链接一年内有效):

https://applink.larkoffice.com/client/chat/chatter/add_by_link?link_token=a0du8c2b-7e0d-4c0c-bb10-7a5b2ff93dda

目前你的飞书账号还不在自动拉群名单里,暂时无法自动邀请;也可以把 GitHub 账号和飞书信息补录到名单文档,补好后后续复审会自动拉你进群:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe

(本条由 botmux 自动评审流程发送)

@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个 PR!以下是自动评审的初步意见,最终以维护者审阅为准。

验证情况

  • 已基于最新 master(ff803e6bc)本地 rebase:零冲突,patch-id 与作者 head 一致;
  • bun run build(tsc)通过;
  • 身份隔离相关 12 个测试文件 674/674 全部通过;fs-policy / sandbox-mask / sandbox-mode 等公共层测试也通过;
  • 对关键安全边界做了反变异验证,以下变异均被现有测试精确杀死:
    • 旧版无 marker 的共享 app HOME 不再清理
    • wrapper 不再 unset 父进程继承的身份 env
    • scratch 漏遮蔽 lark-cli-home/ 或 ~/.larkdev/auth
    • tools 省略时默认扩大为全部三工具
    • 不使用 session 创建时冻结的 sandbox 模式
    • 委托定时闸口改要求全部三工具
    • larkdev wrapper 不重定向 LARKDEV_AUTH_DIR
    • 缺失区域 JWT 时仍注入空值 / 谎报两阶段 stage

整体边界设计是严谨的:lark-cli 应用与用户双重身份都跟发送人走、fail-closed 方向一致、larkdev 显式 opt-in 不静默扩权。

建议修改(均不阻断安全语义)

  1. larkdev wrapper 的临时认证目录只建不清(src/core/cli-identity.ts 的 larkdev-user 分支):每次调用 mktemp -d 后直接 exec,长期运行的 daemon 会持续累积 $TMPDIR/botmux-larkdev-auth/invocation.* 目录(权限有 umask 077 保护,但有 inode/磁盘泄漏,SDK 也可能往里面写缓存)。建议加清理(wrapper 子 shell trap,或 daemon 侧定期清扫该 base 目录)。

  2. config init 的 TUI 自动应答较脆弱(src/services/lark-cli-auth.ts 的 beginPersonalAppSetup):依赖对 Language / 语言、Select action / 选择操作、Select platform / 选择平台 三个文案的正则与固定按键顺序,lark-cli 菜单文案或顺序变化时会在 30 秒内拿不到 URL 而失败(失败方向安全:无 marker 下次重来,不会借用身份)。建议补充在真实 lark-cli 下走通两步授权(feishu 与 lark 双 brand)的验证记录,并注明兼容的 lark-cli 版本范围。

  3. worker 中 larkdev hostOnlyDenyPaths 接线缺少直接测试(src/worker.ts,~/.larkdev/auth 与 LARKDEV_AUTH_DIR 两条):删除这段接线后现有测试仍全绿。fs-policy 机制本身已有覆盖,建议补一条「启用 larkdev → 沙箱策略包含该 deny 路径」的断言。

另外注意到目前 CI 只跑了 assign 检查,测试 workflow 似乎需要维护者批准后才会执行。

Nits(可不阻塞)

  • 沙箱会话每轮都会全量快照 lark-cli HOME,同一发送人连续轮次也重复拷贝(体积 KB 级,影响很小),可按源状态未变化时跳过。
  • mintLarkdevByteCloudJwts 每轮对四个区域并行 spawn bytedcli;账号无某区域权限时每轮都会承担若干个注定失败的进程延迟,可考虑缓存阴性结果。
  • bytedcli provider 侧新增的 scrubByteCloudCredentialEnv 调用没有独立单测(运行时行为由 wrapper 的真实 shell 测试间接覆盖),可补一条 env 断言。

行为变更(供维护者评估,非缺陷)

升级后:① 旧版复制过共享应用的 lark-cli-home/<sender>/ 首次使用会被一次性清理重建;② lark-cli 的 bot-app OAuth fallback 移除。存量用户首次使用需要重新完成「个人应用选择 + 用户授权」两个步骤,建议在发版说明中提示。

@deepcoldy

Copy link
Copy Markdown
Owner

🔴 追加:复审发现 1 个阻断项(P1,需修后重新评审)

二轮复审(独立复核)确认:沙箱会话把当轮发送人的 lark-cli 私有 HOME 快照到了一个基线可读可写的公共目录,导致跨会话/跨 bot 的凭据泄漏。

泄漏路径

materializeLarkCliHomeForSession 把持久 HOME 复制到:

~/.cache/botmux/session-tmp/<dataDir-hash>/<sessionId>/lark-cli-home/...

而 fs-policy 基线对 ~/.cache 是整棵 readWrite(src/adapters/cli/fs-policy.ts 中 commonHomeBaseline 的 rw(\${h}/.cache),linux/darwin 同源)。该快照目录没有任何 deny,而 extraWritePaths 只重新开放「本会话自己的」<sessionId> 目录,挡不住兄弟路径。

用真实 buildFsPolicy 编译 + accessForPath 裁决(两平台一致):

目标 结果
本会话自己的快照 readWrite
另一个会话的快照(含他人 appsecret + user token) readWrite(可读可写/可篡改)
durable 源 ~/.botmux/data/lark-cli-home/<ou>/ deny(本 PR 已正确封住)

<dataDir-hash> 只是 resolve(dataDir) 的哈希,生产上多 bot 共享同一 dataDir,因此同一父目录下所有 bot、所有会话的副本互相可见。改动前凭据只存在于被 deny 的 durable 目录,从不进入 ~/.cache——这是本 PR 新引入的面。

修复方向(已在策略层逐条实证,请按此实现)

  1. fs-policy 沙箱(oncall / 普通 fs-policy,linux bwrap 与 darwin seatbelt 同走 buildFsPolicy):把 ~/.cache/botmux/session-tmp 整体加入 mandatoryDenyPaths(worker 构造 fs-policy 处)。

    • ⚠️ 必须对所有沙箱会话无条件生效,不要加 if (triggerUserAuth) 门控:fs 策略是按会话分别编译的,快照是别的会话在共享父目录里留下的。一个未开启 trigger-user-auth 的会话若策略里没有这条 deny,仍能读到开启该功能的会话所物化的他人快照。(这与本 PR 里 larkdev 那段按条件加 hostOnlyDenyPaths 的接线不同,那段只保护本会话自己的行为,这里防的是任意第三方读取。)
    • ⚠️ 不要用 hostOnlyDenyPaths:buildFsPolicy 对落在 host-only 根内的所有非 deny grant 一律 suppress(不看 source/深度),会把本会话自己 extraWritePaths 的 scratch 目录也一并封掉(实证:host-only 方案下本会话自己的快照也裁决为 deny,功能直接不可用)。mandatoryDenyPaths 是 rank4 deny 但不 suppress,本会话 sessionScratch 由现有更深的 internal readWrite 规则按 longest-prefix 重新开放。实证结果:mandatory 方案下「本会话 readWrite / 兄弟会话 deny / 根 deny」,linux、darwin 均成立。
  2. scratch(COW,不走 fs-policy):在 enumerateScratchSecretPaths 加入 ~/.cache/botmux/session-tmp 的同时,必须为本会话自己的 sessionScratch 加一条可写重绑,否则本会话快照也被 mask 掉:

    • linux src/adapters/backend/scratch-sandbox.ts:在 deny-mask 循环之后,对本会话 scratch 追加 rw bind(可仿照 outbox 的 --bind outbox outbox「later mount wins」形态;现有 readOnlyCarvePaths 只支持只读文件级,不足以开放可写目录)。
    • darwin src/adapters/backend/scratch-sandbox-darwin.ts 做对应处理(注意 darwin scratch 对 ~/.cache 只做写封、读仍可能经原生 symlink 穿透,至少要保证读也封得住兄弟目录)。
  3. 测试(随修复一并提交):

    • test/fs-policy.test.ts:加「同一 dataDir 下兄弟会话的 session-tmp/<sid>/lark-cli-home 裁决为 deny、本会话自己的为 readWrite」纯策略断言(linux+darwin 两组)。
    • 真实 bwrap e2e:仿照现有「per-session 兄弟只读」用例,断言沙箱内读不到兄弟会话快照、读得到自己的(复审方已用真实 bwrap 复现过读写穿透,修复后应转红→绿)。
    • scratch compiler 的 linux/darwin 两条也补对应断言。

其余待改项(维持上一条评论,补充复审意见)

  • P2 larkdev wrapper 的 mktemp -d 认证目录只建不清(inode/磁盘累积),需清理。
  • P2 config init TUI 三段正则自动应答脆弱,缺真机双 brand 验证;建议在 APP_SETUP_URL_TIMEOUT_MS 超时分支把原始输出尾部落日志,便于排障。
  • P2 worker 中 larkdev hostOnlyDenyPaths 接线缺直接策略断言。
  • P3 materializeLarkCliHomeForSession 每轮都先跑一次 lark-cli auth status --verify(同步网络往返,30s 超时)再复制,建议注释说明或节流;非沙箱路径并无这一往返。
  • P3 cpSync 会把 HOME 内的 .botmux-login-challenge(含 device_code)一并带进快照;P1 修复后跨人风险已消除,但在复制 filter 里排除它更干净。
  • P3 beginPersonalAppSetup 里 rmSync/mkdirSync 抛错(如 EACCES)目前被上游吞成通用 /login 指引,不区分「凭据存储异常」,可改进错误分类(当前方向仍是 fail-closed,不影响安全)。

行为变更提醒(供维护者评估,非缺陷):旧版复制过共享应用的 lark-cli-home/<sender>/ 首次使用会被一次性清空重建、lark-cli 的 bot-app OAuth fallback 被移除,存量用户首次需重走「个人应用选择 + 用户授权」两步。

以上为自动评审的初步意见,最终以维护者审阅为准。

@Hyperzsb
Hyperzsb force-pushed the fix/per-sender-cli-auth-isolation branch from ff37355 to 361bde5 Compare October 12, 2026 07:20
@Hyperzsb

Copy link
Copy Markdown
Author

已根据评审意见完成修复,并 rebase 到上游 master 9b8bb024f。重写后的两条 PR 提交为 e092e1264、361bde5a4,author/committer 均为 GitHub 身份 Hyperzsb:

  1. P1:所有 fs-policy 沙箱无条件将 ~/.cache/botmux/session-tmp 加入 mandatoryDenyPaths,仅通过更深的 extraWritePaths 重开当前会话目录;Linux/macOS 纯策略均为“当前会话 readWrite、兄弟会话 deny”。
  2. P1:scratch 枚举器密封共享 session-tmp,Linux 在 deny mask 后 rw-bind 当前会话并重新只读封口;macOS 对共享根做读写 deny,最终仅重开当前会话,且不会扩大为整棵 ~/.cache。
  3. P2:larkdev wrapper 不再 exec 后遗留 mktemp 目录,改为 trap + 保留退出码并清理;真实 shell 用例确认 invocation 目录已删除。
  4. P2:config init 等待 URL 超时时记录清洗后的原始输出尾部与 TUI 阶段;当前参数契约按 lark-cli 1.0.97 核对,继续使用 config init --force-init,不使用会强制创建应用的 --new。
  5. P2:补 worker 接线断言,固定 ~/.larkdev/auth 与绝对 LARKDEV_AUTH_DIR 进入 hostOnlyDenyPaths。
  6. P3:快照排除 .botmux-login-challenge;补充每轮 auth status --verify 的设计注释;凭据目录初始化异常单独记录;bytedcli provider 的宿主 ByteCloud 环境清理新增独立测试。

验证结果:

  • bun run build:通过,runtime build id b05370dacca9
  • 15 个相关测试文件:806 passed, 2 skipped
  • 真实 bwrap 跨会话用例:1 passed,当前会话可读写,兄弟会话凭据不可读写
  • Linux/macOS scratch 编译器顺序与 carve 断言通过
  • daemon 重启后 supervisor、daemon、dashboard 均 online,restart count 0

PR 描述已同步补充无条件沙箱影响面、存量用户两阶段重新授权提示及最新验证结果,请重新评审。

@deepcoldy

Copy link
Copy Markdown
Owner

三轮复审(新 commit 361bde5a):P1 修复正确,另发现 1 个边缘 P2

已基于最新 master(892ff9dee)本地 rebase:零冲突;rebase 后首个 commit patch-id 与原 head 逐字一致,修复 commit patch-id 不变。

✅ P1(跨会话凭据快照泄漏)已按要求修复,验证通过

四项要求全部落实,且没有踩 host-only 的坑:

  1. fs-policy 沙箱:worker.ts 无条件 mandatoryDenyPaths.push(canonical(sessionTempRootDir()))——不经 triggerUserAuth 门控(未开启的会话也封),本会话 sessionScratchDir 经现有更深的 internal readWrite 规则重开。
  2. scratch linux/darwin 双 compiler:enumerator 新增 writableCarvePaths,linux 在 deny mask 之后 later-wins --bind(compileScratchWritableCarveArgs,含 tmpfs/remount-ro 配合);darwin 把含 carve 的宽 deny 走 stage 2 而不提升到 ~/.cache,stage 6 最后精确 reopen 本会话子树。
  3. 纯策略断言(linux+darwin)+ 真实 bwrap e2e(本会话副本可读可写穿透宿主、兄弟会话读写皆拒、篡改不落盘)+ scratch linux/darwin 断言齐全。
  4. 本地验证:bun run build 0 错;16 个相关测试文件 834/834 全绿(含真实 bwrap 用例实跑非 skip);对修复做了 7 个反变异(撤无条件 seal / 不封根 / 不产 carve / linux 不 rebind / darwin 不 reopen / larkdev 不清理 / challenge 重新进快照),全部被现有测试精确杀死。

之前的 P2/P3 处置

  • P2-1 larkdev 临时认证目录清理:已修(trap EXIT HUP INT TERM + 显式 cleanup + 保存并透传退出码),变异可杀。
  • P2-3 worker 接线无断言:已补 WIRING GUARD 字符串守卫,变异可杀。
  • P3:challenge 文件已从快照 filter 排除(带测试);provider scrubByteCloudCredentialEnv 已补独立单测;rmSync/mkdir 失败现在 try/catch 返回 null 并 warn;每轮 auth status --verify 选择保留并补了注释说明(spawn 后 bind 不可变、需先刷新 token)——有意识的权衡,接受。
  • P2-2(维持):config init 超时诊断日志已补(ANSI/控制字符清洗后的 2KB 尾部 + 卡住的步骤名,这点很好);但真实 lark-cli 下 feishu/lark 双 brand 的端到端两步授权仍只能在真机验证,合入前建议维护者安排一次。

🟠 新发现 P2(边缘部署形态,fail-closed 但功能不可用):符号链接 HOME 下 scratch carve 门控失配

enumerateScratchSecretPaths 里 worker 传入的 homeDir 是 realpathSync(homedir())(scratchHome),而 sessionScratchDir 是按词法 homedir() 派生的(sessionTempDir() 内部用词法 homedir()):

const sessionTempRoot = resolve(homeDir, '.cache', 'botmux', 'session-tmp'); // 真实路径
const ownScratch = resolve(input.sessionScratchDir);                          // 词法路径
if (ownScratch.startsWith(`${sessionTempRoot}${sep}`) && isDir(ownScratch)) { // 前缀不等 → 永远 false
  writableCarve.add(ownScratch);
}

在 HOME 是符号链接的主机上(如 /home/u → /data00/home/u,本仓库 codex #605 明确支持且有专门回归测试的形态),词法路径不以真实路径为前缀,writableCarvePaths 被静默丢弃;根 deny 仍然生效(安全方向正确,不泄漏),但本会话自己的 lark-cli 快照在 scratch 沙箱内也被封死,trigger-user lark-cli 整条不可用。linux 与 darwin scratch 同根因(darwin 的 canonical 发生在这个门控之后,救不回来)。普通 fs-policy/oncall 路径两侧都经 canonical,不受影响。

我用符号链接夹具实证:deny has root: true 但 writableCarves: []。

建议修法(门控比较前两侧都归一到真实路径):

const real = (p: string) => { try { return realpathSync(p); } catch { return resolve(p); } };
const sessionTempRoot = real(resolve(homeDir, '.cache', 'botmux', 'session-tmp'));
if (input.sessionScratchDir) {
  const ownScratch = real(resolve(input.sessionScratchDir));
  if (ownScratch.startsWith(`${sessionTempRoot}${sep}`) && isDir(input.sessionScratchDir)) {
    writableCarve.add(ownScratch);
  }
}

并补一条符号链接 HOME 的枚举器断言(仿照 worker 里现有的 lexical-vs-realpath 回归)。

P3(新,信号语义)

larkdev wrapper 改成子进程模式后,信号 trap 只 cleanup 不 exit:若 larkdev 被 SIGTERM/SIGINT 杀死,sh 的 wait 被中断后继续执行 __botmux_larkdev_status=$?,此时 $? 是 trap 内 rm 的退出码(0)而非信号退出码,wrapper 可能以 0 退出(旧 exec 形态透传 130/143)。建议 trap 内 cleanup 后按信号 exit $((128+N)),避免取消/超时被误判为成功。

仍欠的真机验证(不阻断代码评审)

  • macOS scratch 下兄弟会话不可读 + 本会话快照可读写(Seatbelt 只能真机/同核验证);
  • 真实 lark-cli 双 brand 的 config init 两步流程。

以上为自动评审初步意见,最终以维护者审阅为准。整体修复质量很高,P1 的四条都落准了;这个 symlink carve 是本轮唯一建议合入前修掉的项。

@deepcoldy

Copy link
Copy Markdown
Owner

补充一条非阻断建议(交叉复审确认):

scripts/scratch-sandbox-darwin-probe.mjs 目前没有覆盖 session-tmp carve(grep session-tmp 无命中),所以 darwin stage6「shared 根 deny → 本会话子树 read/write allow、兄弟子树不 allow」目前只有 profile 字符串顺序断言,没有真实 Seatbelt 证据。若方便(有 Mac 或 mac CI),建议在该 probe 里加一例:sibling 读被拒、own 可读写。这属于证据缺口、不阻断合入——既有 profile 全线依赖同样的 last-match 语义;但这是本次新增的 carve,补一例最稳妥。

其余结论维持上一条:P1 已验证修复;合入前建议修掉 symlink-HOME 的 carve 门控(门控前两侧 realpath 归一 + symlink 断言);larkdev 信号退出码 P3 可一并修。自动评审初步意见,以维护者审阅为准。

@Hyperzsb

Copy link
Copy Markdown
Author

已按最新评审意见完成修复并推送,当前 head 为 f46f1e131b114003e262ca85993951ffd095c66f。

修复结果

  • 将 ByteCloud 五个 canonical 凭据区域收敛为唯一真源,并在 provider/CLI 子进程启动前清理宿主 ByteCloud、service-account、AI Auth、ZTI 及 credential-like 环境变量,避免跨区域或宿主凭据回退。
  • 普通消息、委托和 schedule 流统一绑定当前发送人的 turn identity;身份缺失、换人、撤权或 provider 失败时保持 fail closed。
  • 修复 symlink HOME 场景:shared session-tmp root 与当前会话 scratch carve 双侧都按 realpath/canonical 路径计算,保留严格后代检查,避免自己的 carve 丢失或 sibling 被放行。
  • 修复 larkdev wrapper 的信号处理:信号转发给子进程、等待真实退出状态、延迟清理临时认证目录,并透传非常规退出码;专项测试使用退出码 42 固定该行为。

验证结果

  • bun run build:通过,runtime build id e071e2b0bf99。
  • test/cli-identity.test.ts + test/scratch-credentials.test.ts:78 passed, 2 skipped。
  • 宿主权限环境复跑 scratch sandbox:18/18 passed;bwrap 身份隔离:2/2 passed。
  • 全量测试已执行:30129 passed, 19 failed, 100 skipped。19 项集中在 root 权限语义、overlay/bwrap、tmux/进程时序和文件描述符耗尽等环境/并发场景;与本次改动相关的权限型用例已在对应宿主环境单独复跑通过。
  • 已执行 bun run switch:here 与 bun run daemon:restart 部署当前 head;宿主状态检查确认 supervisor、daemon 和 dashboard 均在线,已有会话恢复成功。

@deepcoldy

Copy link
Copy Markdown
Owner

四轮复审(新 commit 0ea84f98 跨区域身份回退 + f46f1e13 symlink carve):两项均通过,无阻断

已基于最新 master(510588de1)本地 rebase:零冲突;4 个 commit 的 patch-id 与作者远端逐字一致(rebase 未改内容)。

✅ f46f1e13 symlink-HOME carve(上轮 P2)

门控前两侧都 realpathSync 归一(不存在时保留词法 fallback,不影响可选路径),与建议修法一致;新增真实 symlink 夹具断言(shared 用 canonical、own 进 carve、sibling 不进)。我做了组合反变异(两处 canonical 同时回退到旧词法比较)→ 测试精确变红。

✅ 0ea84f98 封闭跨区域用户身份回退(作者主动加固,质量高)

认可这个修复抓的点:旧实现只发布单区域 BYTEDCLI_USER_CLOUD_JWT(CN),当命令用 --site 选其它区域时 bytedcli 跳过不匹配的 JWT、可能回退到操作员持久登录。本轮把发布面改成 5 站点的 SDK 变量,并做了成套收口,我逐项核过:

  • bytedcli 与 larkdev 现在都在「空 HOME + 仅注入本轮站点 JWT」下运行,缺区域 token 即 fail closed,不读 ~/.local/share/bytedcli;
  • 站点 mint 改为串行(避免同一 durable HOME 下 ByteCloud Auth 刷新/持久化的并发写竞争),合理;
  • 新的 DYNAMIC_BYTECLOUD_IDENTITY_SCRUB 用 env | sed 按 TOKEN/JWT/COOKIE/PASSWORD/SECRET/ACCESS_KEY/AUTHORIZATION 族清理父进程凭证,未来新站点/新子命令凭证默认 fail closed,不再依赖硬编码后缀表;且用 command -p 取系统 env/sed/mkdir/mktemp/rm/kill,防 agent PATH 投毒(有专门测试:PATH 放全套假 helper,真实工具仍拿到正确 JWT、假 helper 零执行);
  • 拒绝 --as / --bytecloud-user-jwt-file 两种身份覆盖参数(4 种写法都测);
  • git askpass 改为当场让「被包装的 bytedcli」用本轮 JWT 换 codebase token(exchange 兜底同步改造,不再读子 wrapper 传不出来的 BYTEDCLI_USER_*);
  • 信号 supervisor:真实 shell 测试验证了 TERM 转发、子进程退出码原样透传(larkdev 用非典型的 42 证明不是合成值)、隔离目录在信号后清空——上轮提的「trap 吞退出码」P3 已闭环。

我额外用本机真实 bytedcli(0.x)核实了 deny-list 的完整性:未拦截的 --profile <name> 数据目录是 $HOME/.local/share/bytedcli/profiles/<name>/data,而 profile 名经 sanitizeFilenamePart(/ → _)无法 ../ 逃逸,空 HOME 下天然无凭据;--auth-site 只切 SSO 环境、不携带凭据。因此 --as/jwt-file 两处拦截 + 空 HOME 主防线已足够,无需扩大参数黑名单。

本地验证:bun run build 0 错;17 个相关测试文件 854/854 全绿(含真实 bwrap e2e、真实 sh/dash 信号与 PATH 投毒用例);沙箱公共层 94 绿;生成的三个 wrapper 与 askpass sh -n/dash -n 语法通过。反变异:动态 scrub 删一族、--as 拦截移除、bytedcli 去隔离 HOME、provider 收窄 scrub、symlink 双回退——全部被测试精确杀死。

非阻断项(P3,不影响合入)

  1. 5 站点 mint 串行化目前没有时序断言(反变异存活)——并发写竞争本身难以单测,注释已说明理由;代价是每轮 4 个串行 spawn 的延迟,可接受,后续可加耗时观测。
  2. 动态 scrub 按 TOKEN/JWT/… 族清理,理论上会误删个别与身份无关的合法 TOKEN 类环境变量;这是 fail-closed 的有意权衡(注释已写明),如真机遇到具体误杀再收窄。
  3. darwin stage6 的 session-tmp carve 仍只有 profile 字符串顺序断言,建议在 scripts/scratch-sandbox-darwin-probe.mjs 补一例真机 Seatbelt(上轮已提,证据缺口、非阻断)。

仍欠的真机验证(只能在有真实账号/Mac 的环境做):真实 bytedcli --site 跨区域命令、git push 经新 askpass 链路换 codebase JWT、真实 lark-cli 双 brand 两步授权、macOS scratch 兄弟不可读。

四轮评审到此无阻断项;以上 P3 与真机项记录在案。自动评审初步意见,最终以维护者审阅为准。

@deepcoldy
deepcoldy force-pushed the fix/per-sender-cli-auth-isolation branch from f46f1e1 to 6014846 Compare October 13, 2026 03:07
deepcoldy pushed a commit to Hyperzsb/botmux that referenced this pull request Oct 13, 2026
整合最新 master(含 deepcoldy#1606 标准会话 lark-cli 绑定):dashboard 授权路由按
boundTool 双轨分流(标准绑定会话走 bot 应用 requestUserAuthorization;
按人鉴权会话走发送人个人 app 的 beginLarkCliLogin),auth-status 轮询按
身份形态分别落 token / HOME,app-token 支不写 trigger-user 身份文件。

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@deepcoldy

Copy link
Copy Markdown
Owner

合入前整合说明(维护者操作,已 force-push 到本分支架)

为解除与最新 master 的冲突,维护者按评审流程把本分支整合 rebase 到了最新 master(ae968b6cf,含已合入的 #1606「标准会话 lark-cli 绑定收消息 bot 应用」),并压成了一个 commit(601484640,保留原作者署名与 TRAE co-author)。原 4 个 commit 的内容全部保留,没有改动你的功能逻辑。

整合需要解决与 #1606 在同一片身份代码上的冲突,其中 3 处是纯文本并集(import 并存、窄孔路由与授权类型并列、一个条件取交集);另有 1 处真实语义分叉做了双轨分流,在此说明,便于你核对:

src/core/dashboard-ipc-server.ts 的授权发起/轮询路由,#1606 与本 PR 对「lark-cli 缺授权时怎么补」采用了不同身份模型,整合后按会话是否为「标准绑定会话」(boundTool,带 x-botmux-lark-session 会话密钥)分流:

  • 标准绑定会话(feat(lark-cli): 为标准新会话绑定当前机器人应用身份 #1606 的 lark-tool 会话):auth-request 仍走 requestUserAuthorization(用 Bot 自己的应用换当前轮用户的 user_access_token),auth-status 就绪后不写 trigger-user 身份文件(这类会话由窄孔 lark-tool-identity 路由实时发 token)。这与 worker 里已有的分工一致——标准绑定会话会把 lark-cli 从 trigger-user 工具集中剔除,lark-cli 归 Bot 应用、bytedcli/larkdev 归本 PR 的 wrapper。
  • 按人鉴权会话(本 PR):维持你的 beginLarkCliLogin 发送人个人应用两步流程,auth-status 就绪后把当轮个人 HOME 以 mode: 'user-home' 写入身份文件(沙箱会话仍走会话内快照)。

整合树验证:bun run build 0 错;#1796 与 #1606 两线共 19 个测试文件 520/520 全绿(含真实 bwrap、真实 sh/dash、PATH 投毒、窄孔端到端);对「标准绑定会话被错误导到个人应用流程」做了反变异,会精确变红。

如果这个双轨分流的处理方式你有不同意见,欢迎在本 PR 评论;否则评审通过、维护者确认后即可合入。感谢高质量的四轮迭代。自动评审整合说明,最终以维护者审阅为准。

整合最新 master(含 deepcoldy#1606 标准会话 lark-cli 绑定):dashboard 授权路由按
boundTool 双轨分流(标准绑定会话走 bot 应用 requestUserAuthorization;
按人鉴权会话走发送人个人 app 的 beginLarkCliLogin),auth-status 轮询按
身份形态分别落 token / HOME,app-token 支不写 trigger-user 身份文件。

Co-authored-by: TRAE CLI <traecli@bytedance.com>
@deepcoldy
deepcoldy force-pushed the fix/per-sender-cli-auth-isolation branch from 6014846 to be1995c Compare October 13, 2026 03:40

This branch has not been deployed

No deployments
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.

2 participants