Skip to content

feat(trigger-user-auth): lark-cli 设备码按人鉴权 + 统一自动授权(收敛 #1382) - #1384

Merged
deepcoldy merged 7 commits into
masterfrom
feat/trigger-user-auth-unified
Sep 21, 2026
Merged

deepcoldy merged 7 commits into
masterfrom
feat/trigger-user-auth-unified

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

背景

按触发人(飞书消息发送者)让 lark-cli / bytedcli 以其本人身份执行。本 PR 在 #1382(bytedcli 缺凭证自动发起设备码 + /login 对 canTalk 放权)之上,把 lark-cli 拉齐到同一套设备码/按人 HOME/自动授权体验,收敛成一个完整方案。

本分支基于 #1382 的 head(34ad79a2e,其 parent 即当前 master)。建议与 #1382 一起合并;#1382 合入后本 PR diff 会自动只剩 lark-cli 增量(11 文件)。

改动(相对 #1382 的增量,11 文件)

  • 新增 services/lark-cli-auth.ts:per-person HOME ~/.botmux/data/lark-cli-home/<openId>/;设备码 auth login --no-wait/--device-code;从机器级 issuer HOME bootstrap app 凭证(master.key+appsecret);未过期 challenge 复用(同轮不重复弹)。签发 app 来源:BOTMUX_LARK_CLI_ISSUER_HOME > 托管 ~/.botmux/data/lark-cli-app-bootstrap > 操作员 lark-cli HOME。
  • cli-identity.ts:新增 user-home 身份模式,wrapper 以 per-person HOME 执行 lark-cli(不注入 user token);保留 turn 绑定、缺失目录即拒(exit 77)。
  • turn-cli-identity.ts:lark-cli 优先 per-person HOME(回退旧 bot-app user token);lark-cli 缺凭证时也自动 begin 设备码、把有效链接嵌入拒绝 stderr(与 bytedcli 同款)。
  • /login:新增 /login lark 设备码入口、/login done 同时收尾两种设备码;/login status 按 per-person HOME 分报 lark/byted 状态。
  • 授权体验(产品硬要求):文案统一「只需授权一次、约 7 天(lark)/3 周(bytedcli)自动续期、点完直接重试、无需命令」,lark/byted 都带 provider;新增 agent 规则 on_auth_link:拒绝 stderr 含链接时用 botmux send 逐字原样转发、停下等授权、禁止换身份/假装成功。
  • dashboard:修「按触发人鉴权」工具勾选框在 .bd-row grid 下被压成两行(checkbox 居中在文字上方)的错位(css specificity 1:1 覆盖)。

关键事实(飞书身份层,代码绕不过,写入说明)

  • lark-cli 无内置「全员通用」app:设备码/OAuth 必须绑一个自建飞书 app,且授权用户必须在该 app 可用范围内。
  • 实操正解 = 每人/统一使用一个**自己有权限(创建者天然在内)**的 app 作签发方;per-person HOME 已为此隔离。真机三用户验证:自建 app 设备码可过、JWT/token 身份正确(sunxiaoxue / 董思琪)、跨人不串;bytedcli 走 ByteCloud SSO 无此范围闸。
  • 「飞书后台扩大应用可用范围审批」是外部前提,本 PR 不绕过。

影响面

  • 鉴权交互 + lark-cli 身份来源;未开 triggerUserAuth 的 bot、其它 CLI/后端/会话类型、服务器侧 bot-token SDK 调用(附件下载/文档评论)不受影响。
  • 默认 fallback 保持 fix(trigger-user-auth): 自动发起 bytedcli 登录并开放 /login #1382 现状(bot-identity);lark-cli 用户向主路径改为 per-person HOME 设备码。

验证

  • tsc --noEmit 0;bun run build 通过
  • 直接相关 11 文件 770 测试全绿;wrapper/sandbox/identity 等 12 文件 216 绿
  • 反变异:默认回退 bot、拒绝不预取链接、去掉 challenge 复用 均转红
  • 真机部署 dist 端到端:lark(accounts.feishu.cn)/byted(cloud.bytedance.net) 未授权拒绝均含正确 provider 的有效设备码链接 + 一次授权文案;真人扫码授权后以本人身份成功读飞书文档

@deepcoldy

Copy link
Copy Markdown
Owner Author

已按决定关闭 #1382:其全部改动(bytedcli 自动登录 + /login 对 canTalk 放权,commit 34ad79a2e)作为本 PR 的基础 commit 一并包含。本 PR 现独立自洽,直接合本 PR 即同时带上 #1382 内容,无需先合另一个。

@deepcoldy

Copy link
Copy Markdown
Owner Author

本地测试反馈后补一版(commit 0c29a0c):

  1. lark-cli 与 bytedcli 对齐:开了按触发人鉴权但用户没 HOME 身份时,不再静默降级 bot(bot 对 docs/drive/wiki/search 等 user-only 命令不可用),直接拒绝并内嵌该用户可授权的设备码链接,不用手动 /login。
  2. 签发 app 按发起人解析:优先 ~/.botmux/data/lark-cli-issuer-<openId>(每人各自 config init --new 建的、自己有权限的 app),从根上消除「点开标『你没有该 app 使用权限』」的错误链接;真机验证两个用户各自 begin 出的链接绑定各自的 app。
  3. 无人类发起人(定时/hook)仍 deny 但不取链接、不打扰。
    755 相关测试绿、tsc/build 过、已部署真机验证。

@deepcoldy

Copy link
Copy Markdown
Owner Author

自动评审的初步意见,供参考;最终以维护者审阅为准。基线已同步到 e8c9bbdfa(本地 rebase 验证,未改动本 PR 远端提交),在该基线上 tsc --noEmit / bun run build 均 exit 0,本 PR 涉及的 6 个测试文件 479/479 通过,远端 CI 13/13 绿。

整体方向认同:把 lark-cli 从「注入 user_access_token」改成与 bytedcli 一致的「一人一个 HOME 目录」,<appId>_<openId>.enc 严格不复制、只 seed 机器级 app 材料,wrapper 缺目录 exit 77 fail-closed,HOME=... exec 用内联赋值不 export,hasLarkCliHome 要求 config.json 且至少一个真 token 文件——这些都处理得很干净。下面两条是希望合入前再看一眼的问题。

1. lark-cli 的「点完链接直接重试」在当前实现下无法完成

lark-cli 的设备码是两段式:--no-wait 只拿链接,用户在浏览器点完授权本地不会写入任何东西,必须再调用一次 auth login --device-code <code> 轮询,token 才会落到该 HOME。

而 completeLarkCliLogin 在 src/ 里只有一个生产调用方——src/core/command-handler.ts:3322,即 /login done 分支。turn 路径(resolveIdentityFor → larkCliHomeForTurn)只检查目录是否存在,从不轮询。

结果是:用户点完链接、按提示让 bot 重试 → hasLarkCliHome 仍为 false → 再次被拒 → 再贴同一条链接(beginLarkCliLogin 会复用未过期的 challenge),一直循环到用户自己想到要发 /login done。而拒绝文案 trigger_user_auth.denied_auto_retry 明确写的是「点完直接让我重试刚才的操作就行,不用再发任何命令」,ai.credentials.on_auth_link 也在教 agent 转发链接后等待重试——两边都在把用户往这个循环里推。首次 /login lark 的回复里也没有提到 /login done(相关的 cmd.login.lark_step2 在 zh/en 都已定义但零处使用)。

bytedcli 没有这个问题:mintBytedcliJwts 会先把 pending challenge 收尾再取 JWT,所以「点完重试」对它是成立的。两边契约目前不对称。

用真实契约建模(只有 --device-code 会写 .enc)跑端到端验证:turn1 的拒绝文案里确有链接和那句提示,模拟浏览器授权完成后 turn2/3/4 仍全部是 needs-authorization;显式调用一次 completeLarkCliLogin 后立刻变成 user + user-home。照 mintBytedcliJwts 的写法在 resolveIdentityFor 里补一次收尾后,上述端到端用例转红(即该路径确实由它覆盖)。

建议:resolveIdentityFor 的 lark-cli 分支,在 hasLarkCliHome 为 false 且存在 pending challenge 时,先 await completeLarkCliLogin(openId) 再判目录,与 bytedcli 逐字对称。独立验证过这个改法:pending 状态下每轮 1 次轮询(与 bytedcli 现状同量级);未授权者补收尾后仍 pending、无 home,继续被拒,安全边界没有放松;OAuth back-compat 路径无回归。

2. 一次网络抖动会把已授权的 bytedcli 用户锁在门外

mintBytedcliJwts 开头是硬闸:只要存在 pending challenge,就先轮询,结果不是 authorized 就 return null,不会去读这个人本来有效的登录态。而 completeBytedcliLogin 只在 ok 时清 challenge,pending / failed 都不清;beginBytedcliLogin 这版又改为复用未过期 challenge,所以它会一直卡到 50 分钟 TTL 自然过期。clearBytedcliAuth 目前没有任何生产调用方,也没有 /logout 可以手动解开。

本 PR 新增的 withholdIdentity 自动 begin 放大了这条链路:一次取 JWT 的网络抖动 → 自动 begin → 写下 challenge 文件 —— 一个本来已授权的用户就此把自己锁住。相对当前 master 这是新增行为:master 的 mintBytedcliJwts 没有这个硬闸(if (!hasBytedcliHome) return null 后直接取 JWT),withholdIdentity 也是同步的、不会自动 begin。

验证:turn1 让取 JWT 超时一次后落下 challenge;turn2 网络已恢复,仍返回 needs-authorization,且 get-bytecloud-jwt-token 一次都没有被调用;turn3/4 相同,challenge 始终不滚动;删掉 challenge 后同一用户立刻恢复正常。另有用例分别覆盖 pending / failed / 无 challenge / challenge 正常完成四种情况。

建议二选一:① 把硬闸改成尽力而为——收尾失败时不要 return null,落回「先试这个人已有的登录态」;② 至少在 failed 时清掉 challenge,不要让一次失败粘住 50 分钟。

3. 建议同 PR 一并处理(不阻断)

  • /status 的说法与实际行为不符。src/ 中已无任何地方产出 lark-cli 的 state: 'bot-identity' / mode: 'bot',但 triggerUserAuthStatusLines(src/core/command-handler.ts:1284)这次没有跟着改,botFallback 现在恒为真,未授权的用户会被告知「当前以 bot 身份调用」,而实际上命令会被直接拒绝。同一函数里的 larkAuthorized 读的仍是旧的 listAuthorizedUsers(user-token-<appId>-*.json),不看 hasLarkCliHome,所以走新设备码流程授权过的人在 /status 里显示为未授权,与已经改对了的 /login status 口径相反。顺带可以清掉现在不可达的 ToolIdentityOutcome.state 中的 'bot-identity',以及 Dashboard 选项与 src/services/bot-config-store.ts:133 里仍描述「降级到 bot 自己的身份」的 hint。PR 描述中「默认 fallback 保持 fix(trigger-user-auth): 自动发起 bytedcli 登录并开放 /login #1382 现状(bot-identity)」一句也与当前代码相反。
  • /login done 的两条链路互斥。src/core/command-handler.ts:3321 的条件是 pendingLarkCliChallenge(...) || hasLarkCliHome(...),进入即 replied = true,下方的 bytedcli 收尾永远够不到。已授权 lark、且有 pending bytedcli challenge 的用户发 /login done,只会收到 lark 分支的「没有进行中的登录,请重新开始」。建议两条各自独立判断。
  • cmd.login.lark_step2(src/i18n/zh.ts:458 / src/i18n/en.ts:460)定义后未使用。它正好是第 1 条缺失的那句提示,用上可以缓解可发现性问题(但不能替代第 1 条的修复)。
  • 时延:withholdIdentity 由同步改为 async,内部可能发起最长 30s 的外部调用(LARK_CLI_TIMEOUT_MS / BYTEDCLI_TIMEOUT_MS),每个被治理的工具每轮各一次;而 prepareTurn / prepareRawInputTurn 在 5 处调用点均为 await,处于 turn 的关键路径上。两个工具都拿不到身份时,最坏情况是一轮开头阻塞约 60s。建议评估加超时上限、并发化,或移出关键路径。

以上验证用的临时探针均已删除,未对本仓库留下任何改动。

@deepcoldy
deepcoldy force-pushed the feat/trigger-user-auth-unified branch 3 times, most recently from 9148611 to a6cea38 Compare September 21, 2026 17:22
xiaoxueSunn and others added 7 commits September 21, 2026 11:08
在 #1382(bytedcli 自动登录 + /login 放权)基础上,把 lark-cli 拉齐到同一套
按触发人鉴权/设备码体验,并把授权链接做成统一、保证有效、一次授权的形态:

- 新增 services/lark-cli-auth.ts:per-person HOME(~/.botmux/data/
  lark-cli-home/<openId>)、设备码 begin/complete、机器级 app 凭证 bootstrap、
  未过期 challenge 复用;签发 app 来源支持 BOTMUX_LARK_CLI_ISSUER_HOME 与
  托管 ~/.botmux/data/lark-cli-app-bootstrap,避免隐式取操作员首个 app。
- cli-identity:新增 user-home 身份模式,wrapper 以 per-person HOME 执行
  lark-cli(不注入 user token),保留 turn 绑定/缺失目录即拒。
- turn-cli-identity:lark-cli 优先用 per-person HOME(回退旧 bot-app token);
  lark-cli 缺凭证时也自动 begin 设备码并把链接嵌入拒绝信息(与 bytedcli 同款)。
- 授权文案统一为「只需授权一次、自动续期、点完直接重试」,lark/byted 都带
  provider,不出现「无权限错误路由」式生硬提示;新增 agent 转述规则 on_auth_link。
- /login 增加 lark 设备码入口(/login lark、/login done 同时收尾两种设备码),
  /login status 按 per-person HOME 报 lark/byted 各自状态。
- 修 dashboard「按触发人鉴权」工具勾选框在 bd-row grid 下错位(css specificity)。

注:飞书没有「全员通用 CLI app」,设备码必须绑自建 app 且用户在其可用范围;
正解是每人/统一使用自己有权限(创建者天然在内)的签发 app,per-person HOME
已为此隔离。飞书后台「扩大可用范围」审批是外部前提,代码不绕过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
…me 用例

Co-Authored-By: Claude Code <noreply@anthropic.com>
对齐 bytedcli:lark-cli 开启按触发人鉴权后,发起人没有 per-person HOME
身份时不再静默降级 bot(bot 身份对 docs/drive/wiki/search 等 user-only
命令本就不可用,静默降级只会失败且不给链接),而是直接拒绝并内嵌该用户
可授权的设备码链接——「用到、没授权 → 直接给链接」,无需手敲 /login。

签发 app 按发起人解析,杜绝「点开标『你没有该 app 使用权限』」的错误链接:
issuerMachineHome(openId) 优先级 test > env > **per-person
~/.botmux/data/lark-cli-issuer-<openId>**(每人各自 config init --new 建的、
自己天然有权限的 app)> 共享 bootstrap > 操作员 HOME。openId 做路径字符白名单
防穿越。真机验证孙晓雪/董思琪各自 begin 出的链接绑定的是她俩自己的 app。

- turn-cli-identity:lark 与 byted 同款 deny+link,移除 lark 静默 bot 分支
  及随之无用的 unauthorizedOutcomeFor import;对应测试改为 needs-authorization。
- 无人类发起人(定时/hook)仍 deny 但不取链接、不发消息。

Co-Authored-By: Claude Code <noreply@anthropic.com>
修两个会把人锁在授权循环外的阻断问题:

- lark-cli 设备码是两段式:浏览器点完本地零写入,必须再用 --device-code
  轮询一次,token 才会落进按人 HOME。此前轮询只在 /login done 里发生,
  turn 路径只查目录,导致拒绝文案承诺的「点完直接重试」必然再次被拒、
  循环到用户自己猜到 /login done。新增 resolveLarkCliHomeForTurn,在
  判定前对未完成的 challenge 轮询至多一次,与 bytedcli 的
  mintBytedcliJwts 逐字对称;未授权者轮询后仍无 HOME,继续被拒。
- bytedcli 的 challenge 收尾此前是硬闸:轮询非 authorized 就直接
  return null,根本不读这个人本来有效的登录态;叠加被拒时自动 begin,
  一次网络抖动就能把已授权者锁到 50 分钟 TTL。改为尽力而为:无论轮询
  结果如何都落回 HOME/JWT 读取,由后者作权威判定;终态 failed(含
  lark-cli)同时清掉死 challenge,下一次 begin 换新链接,pending
  保留以便继续扫码。
- 顺手修掉 #1382 的目录误判:runAsUser 每次调用都会 mkdir HOME,
  hasBytedcliHome 只判目录存在会把「发起过但没授权」报成已授权;
  改为要求 bytedcli 数据目录下存在真正的凭证文件(token[.<env>].json
  或 sso_session[.<env>].json,已对照本机 bytedcli 安装核实布局)。
- ToolIdentityOutcome.state 移除已不可达的 'bot-identity'(lark-cli
  的 bot 降级路径已在本 PR 删除,没有任何产出方)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
- /status 的 trigger-user auth 行此前把「未授权」描述成「当前以 bot
  身份调用」,但 lark-cli 的 bot 降级已移除,实际命令会被直接拒;且
  授权判定只读旧的 user-token 库,走设备码按人 HOME 授权过的人会被
  显示成未授权。改为 HOME 或旧 token 任一存在即「以你自己的身份」,
  未授权一律描述为拒绝并附自动链接;bytedcli 行同样只看真实 HOME。
- /login done 此前用 replied 互斥:lark 有 challenge 或已授权时,
  pending 的 bytedcli 收尾永远够不着,已授权 lark 的人只会收到
  「没有进行中的登录」。改为两个身份提供方各自独立判定、一次回复,
  无 challenge 但已授权时直接报已授权。
- /login lark 与裸 /login 的授权引导补上 lark_step2(此前该文案键
- 定义后零使用):点完链接直接重试即可,/login done 仅作确认路径;
  lark_no_challenge 换成两个提供方通用的 no_challenge。

Co-Authored-By: Claude Code <noreply@anthropic.com>
fallback 字段当前对两个被治理工具都不再产生降级行为(bytedcli 从来
没有 bot 身份,lark-cli 的 bot 降级随按人 HOME 模型移除),但设置页
仍提供「降级到 bot 自己的身份」选项、配置 hint 也这么描述。设置页改
为只读的实际行为说明;解析器与类型保留该字段以兼容存量配置,注释改
为「仅兼容保留」。

Co-Authored-By: Claude Code <noreply@anthropic.com>
@deepcoldy
deepcoldy force-pushed the feat/trigger-user-auth-unified branch from a6cea38 to c422362 Compare September 21, 2026 18:14
@deepcoldy
deepcoldy merged commit d87ab3e into master Sep 21, 2026
12 of 14 checks passed
@github-actions

Copy link
Copy Markdown

🚀 Released in v3.27.0

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