Skip to content

feat(vc-agent): 入会自动 provision lark-cli profile + 失败通知 owner + 沙盒改用户 opt-in - #782

Merged
deepcoldy merged 3 commits into
masterfrom
vc-meeting-join-profile-sandbox-optin
Aug 12, 2026
Merged

feat(vc-agent): 入会自动 provision lark-cli profile + 失败通知 owner + 沙盒改用户 opt-in#782
deepcoldy merged 3 commits into
masterfrom
vc-meeting-join-profile-sandbox-optin

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

背景

会议智能体(VC consumer)在实际使用中发现三类问题,live 测试又暴露两处后续问题,本 PR 一并修复。核心诉求(产品决策):把是否开沙盒的选择权交还给用户——默认不强制沙盒,用户知情后可自行开启隔离。

改了什么 & 为什么

1. 拉不进会(根因,静默失败)
入会走 lark-cli vc +meeting-join --as bot --profile <larkCliProfile>,但 botmux 从不创建这个 profile——默认 larkCliProfile === bot 的 appId(dashboard 自动填),却没有任何地方把它注册进 lark-cli。首次真实邀请时 profile not found,bot 卡在「呼叫中」永远进不去,且失败只写一行日志。

  • daemon 入会前用配置里已存的 larkAppSecret 幂等补建 profile(lark-cli profile add --app-secret-stdin,secret 走 stdin 不落 argv、只进 lark-cli 加密 store、在可信 daemon 进程做,不碰 worker/沙盒)。
  • 注意:lark-cli profile list 不认 --format json(默认即 JSON);profile addalready exists 兜底当成功。

2. 入会失败可见化
入会失败不再只吞一行 WARN,而是 DM 会议 owner 附具体原因(缺 secret / profile 建失败 / 无 meeting_no 等),按会话去重不刷屏。

3. 沙盒改用户 opt-in(产品决策)
evaluateVcMeetingConsumerIsolation 从「硬 require 沙盒」改为拆分两个概念:

  • eligible(可否作为 consumer):默认 true,仅当显式勾了沙盒但当前平台/后端给不了(macOS Seatbelt 无 host relay / riff 远端 / herdr/zellij 未被本地 bwrap 包裹)才 fail-closed——绝不把显式的隔离请求静默降级成裸跑;
  • isolated(沙盒边界是否真在):驱动风险提示。
    全链路改名 managedSideEffectIsolationmanagedSideEffectEligible + sandboxIsolated(daemon gate / bootstrap 选择器 / dashboard API DTO / UI / i18n)。dashboard 对未开沙盒的 bot 显示凭证暴露风险提示(会议是不可信多方输入 + bot 凭证在进程 env,注入可外泄)。

4. receiver 会话 spawn 写死 sandbox=true(live 暴露)
创建会议 receiver 会话时有一行写死 session.sandbox = true。上面第 3 点只改了准入门,漏了这行——未开沙盒的 bot 的 receiver 仍被强制塞沙盒启动,而其环境没为 bwrap 配好(工作目录不在白名单等)→ CLI 起不来/跑不动 → 进了会但永远不回复。改为跟随 isolation.decision.isolated,sandbox 相关路径字段一并跟随。

5. dashboard 下拉直接禁选不可用的 bot(live 反馈)
不能用的执行 Bot(无工作目录 / 不支持可靠回执 / 显式开了沙盒但平台给不了)直接在下拉里禁选(灰掉),而不是等开会才报错。离线是暂时的(不禁)、未开沙盒是知情选择(不禁,只标风险)。

影响面

  • 仅影响 VC 会议 consumer 路径;evaluateVcMeetingConsumerIsolation 是 gate / filter / UI 显示,实际沙盒由 spawn 时 session.sandbox 驱动
  • 跨平台:macOS / riff / herdr / zellij 在显式开沙盒时仍 fail-closed(不静默降级);未开沙盒各平台均可正常入会。
  • profile 自动补建只在可信 daemon 进程执行,凭证不进 worker / 沙盒 / argv。

测试验证

  • pnpm build
  • 新增 test/vc-meeting-join-profile.test.ts(11 用例,注入式覆盖 no-op / provision / missing_secret / add_failed / already-exists 兜底 / list 不可用);
  • 重写 test/vc-meeting-consumer-isolation.test.ts(plan B 语义:未开沙盒 eligible+not isolated、显式开但不可交付 fail-closed);
  • test/vc-meeting-daemon-session.test.ts:未开沙盒的 consumer 从「被拒」改为「被接受」,并新增断言 receiver 会话 sandbox === false(拿老的写死 true 一测即红)+ 「provision 失败→跳过 join+DM owner」;
  • 相关 6 个套件全绿:vc-meeting-join-profile / vc-meeting-daemon-session / vc-meeting-consumer-isolation / vc-meeting-consumer-profile-bootstrap / dashboard-vc-consumer-profiles-api / dashboard-vc-consumer-profiles-ui229 passed
  • Live:已 switch:here + daemon:restart 在飞书实测——入会正常;沙盒修复后 receiver worker 成功 spawn 并投递首轮 prompt(日志验证)。默认 minutes profile 为沉默型(仅在关键决策/待办/风险或被点名时输出),闲聊不回属设计预期。

待补(follow-up,不阻塞本 PR)

  • receiver 冷启动新 claude 会话有十几秒延迟,极短会议(<1min)可能来不及产出——可考虑预热/提示;
  • 如需「会议内可对话、被点名即答」的助手,可加一个 responseMode 更主动的 profile 预设。

…opt-in

修 3 个问题 + 后续 live 测试暴露的 2 个:

1. 拉不进会(根因): 入会走 lark-cli --as bot --profile <larkCliProfile>,但 botmux
   从不创建该 profile→缺失时静默失败卡呼叫中。daemon 入会前用已存 larkAppSecret 幂等
   补建(--app-secret-stdin,不落 argv/worker/沙盒);profile list 不认 --format json
   (默认即 JSON);add 报 already exists 兜底当成功。
2. 入会失败 DM 会议 owner 附具体原因,不再只吞一行 WARN。
3. 沙盒(方案 B): VC consumer 默认不再强制沙盒,不开也能入会;仅当显式勾了沙盒但平台/
   后端给不了才 fail-closed(不静默降级)。evaluateVcMeetingConsumerIsolation 拆
   eligible/isolated 两概念,全链路改名 managedSideEffectIsolation→
   managedSideEffectEligible+sandboxIsolated;dashboard 对未开沙盒 bot 标凭证暴露风险。
4. receiver 会话 spawn 写死 sandbox=true → 方案 B 下未开沙盒的 bot 仍被强制塞沙盒启动,
   环境没配 bwrap→CLI 起不来→进会但不回复。改为跟随 isolation.decision.isolated,
   sandbox 路径字段一并跟随。回归测试断言未开沙盒 bot 的 receiver sandbox===false。
5. dashboard 下拉直接禁选不可用的 bot(无工作目录/不支持可靠回执/显式沙盒但平台给不了),
   不再等开会才报;离线/未开沙盒不禁(前者暂时、后者知情选择)。

测试: 新增 test/vc-meeting-join-profile.test.ts(11);重写 consumer-isolation 测试
(plan B 语义);flip daemon-session「unsandboxed 被拒」→「被接受+receiver sandbox===false」;
改 bootstrap/api/ui 字段名。VC 全套绿。

影响面: 仅 VC 会议 consumer 路径;evaluateVcMeetingConsumerIsolation 是 gate/filter/UI
显示,实际沙盒由 spawn 时 session.sandbox 驱动。跨平台: macOS/riff/herdr/zellij 显式
开沙盒仍 fail-closed。
@deepcoldy

Copy link
Copy Markdown
Owner Author

复审结论:暂不建议合并。发现 1 个核心遗漏和 1 个 dashboard 绕过。

  1. [阻塞] 只有 meetingNo 的邀请在 eager-join 失败时仍然静默。 这条分支在 src/daemon.ts:14937-14951 catch 后只写 WARN,随后因 meeting.id 仍为空直接 return;notifyVcMeetingInviteFailure 位于 session 创建之后,走不到。影响不只 provisioning add_failed:缺少 larkCliProfile 会被外层条件直接跳过,joinMeetingAsBot 的权限/CLI/响应异常也都会静默。建议在这个前置分支失败时直接通知 owner(session 可传 undefined),并新增 meeting: { id: '', meetingNo: ... } + profileProvisionResults=failed 的回归用例,断言 owner DM。

  2. [建议一并修] disabled bot 仍可被新建预设默认选中并保存。 addProfile / addProfileFromTemplate 都固定取 state.agentOptions[0];agentOptions 按 appId 排序,因此首项可能正是 workingDirReady=false / reliableTurnTerminal=false / managedSideEffectEligible=false 的 disabled 项。下拉禁用只阻止用户再次点击该项,不能阻止它作为当前值,PUT 也只校验 agentAppId 非空,所以仍能保存不可用 bot。建议默认选择第一个 structurally eligible 项,并在无 eligible 项时禁用新增/套用模板;补一个“首项 blocked、次项 eligible”的 UI 用例。

实际验证:

  • pnpm build:通过
  • 6 个相关 unit 文件:229/229 通过
  • pnpm test:全量 unit 通过
  • 本机 lark-cli 1.0.76 核对:profile list 默认输出数组 JSON,profile add --app-secret-stdinvc +meeting-join 参数均存在
  • git diff --check origin/master...HEAD:通过

当前 worktree 保持干净,未修改 PR 代码,也未重启 live daemon。

复审发现两处遗留问题,一并修复:

1. eager-join 静默失败:只带 meeting_no(无 meeting.id)的邀请会先
   eager join 以解析 meeting.id。此前该分支 provision/join 失败只写
   一行 WARN,随后落到「no meeting id yet」静默 return——正是最初
   「profile not found → bot 一直呼叫」bug 的发生地,全程无用户可见
   信号(此时还没有 session,走不到 manual-invite 的 owner DM)。且
   旧 `&& cfg.larkCliProfile` 守卫让「缺 profile」直接跳过整块也静默。
   现改为:只要有 meeting_no 就进入该块,任何失败都 DM 会议 owner,
   按 meeting_no 去重(30min TTL,成功入会即清)避免重投邀请刷屏。

2. dashboard 下拉禁选被默认值绕过:新建预设 / 套用模板都固定选
   agentOptions[0](按 appId 排序首项,可能正是被 disabled 的不可用
   bot),而服务端 PUT 仅校验 agentAppId 为非空字符串、无 eligibility
   校验 → 不可用 bot 可被存成 consumer,进会静默不回复。现抽出单一
   谓词 isAgentSelectable(下拉 disabled 与默认播种共用,杜绝漂移),
   默认改选第一个 structurally-eligible bot;无 eligible 时禁用「新增
   预设」与「套用模板」,不再让用户建出永不回复的预设。

测试:
- 新增 3 个 daemon-session 用例(eager 无 id provision 失败 → DM owner、
  重投邀请 DM 去重)+ 2 个 dashboard UI 用例(新建预设播种可用 agent
  而非禁用的 [0]、无 eligible 时 Add 禁用);变异验证均 red-on-revert。
- pnpm build 通过;6 个 VC 套件 233/233 通过。

影响面:仅 VC 会议 consumer 路径。daemon 侧改动在 handleVcMeetingPush
的 meeting_invited eager-join 分支;dashboard 侧为纯前端预设编辑面。
@deepcoldy

Copy link
Copy Markdown
Owner Author

复验 56295f257 结论:前次两个阻塞项均已闭合,未发现新的合并阻塞问题。

  • eager-join:外层 profile 守卫已移除;只有 meeting_no 时,provision / join 任一失败都会在 session 创建前通知 owner,并按 bot + meeting_no 做有界 TTL 去重。新用例覆盖 no-id 失败通知和重复邀请去重。
  • dashboard:isAgentSelectable 同时驱动下拉 disabled、默认 agent 播种和“是否允许新增/套用模板”;首项不可用时会选第一个可用 agent,无可用 agent 时 Add / Use 均被禁用。
  • 服务端 PUT eligibility 硬校验:本轮不建议作为阻塞项。eligibility 会随工作目录、CLI 能力、sandbox 平台/后端变化;直接对整份 PUT 硬拒会让已保存后变为不可用的旧预设无法修改无关字段。当前前端禁止新选/错误播种,运行时仍 fail-loud,分层足够。若未来做服务端加固,建议只拒绝新引入/变更到不可用 agent,保留旧配置的修复通道。

独立验证:

  • pnpm build:通过
  • 6 个相关 unit 文件:233/233 通过
  • pnpm test:13299 passed;仅两个已知并发 load-flake 超时
  • 上述两个 load-flake 隔离重跑:32/32 通过
  • GitHub CI / CodeQL:全部通过,head=56295f257,MERGEABLE
  • git diff --check origin/master...HEAD:通过;worktree 干净

两个非阻塞测试卫生建议:__vcMeetingAgentTest.reset() 可顺手 clear 新增的 eager failure map,避免未来复用 meeting_no 的用例产生顺序依赖;模板 Use 的无 eligible 禁用目前由共用布尔值保证,也可补一条显式 UI 断言。均不影响本次结论。

复审两个非阻塞建议,收口测试隔离性与覆盖:
- __vcMeetingAgentTest.reset() 一并 clear 新增的
  vcMeetingEagerJoinFailureNotified 去重表,避免跨用例的 meeting_no
  去重状态泄漏成潜在 flake(当前两个 eager 用例用不同 meeting_no
  暂不撞,但补齐防回归)。
- 「无 eligible agent」用例补断言:打开模板详情后其「套用」按钮也
  必须 disabled——套用模板同样会播种 agent,不能成为绕过禁选、
  建出永不回复预设的第二条路径。变异验证:把该 gate 还原成旧的
  length===0 形态即 red(全 disabled 列表 length=1 会误放行)。

纯测试 + 一行 reset,无生产行为变化。build ✅;
vc-meeting-daemon-session + dashboard-vc-consumer-profiles-ui 143/143。
@deepcoldy

Copy link
Copy Markdown
Owner Author

补充验证最新 head c69c8b6e5

  • commit diff 仅包含 eager-join 去重表的测试 reset 清理,以及“无可选 agent”时模板 Use 按钮禁用断言;无生产行为变化,git diff --check 通过。
  • 受影响套件实跑:vc-meeting-daemon-session + dashboard-vc-consumer-profiles-ui,143/143 通过。
  • 首轮 CI 唯一失败为未触及的 platform-http-family IPv6/localhost 环境用例;该文件隔离复跑 3/3 通过,重跑 CI 后 build、全量单测及 workflow-core 全部通过。

未发现新增阻塞项。

@deepcoldy
deepcoldy merged commit f11c0c9 into master Aug 12, 2026
6 of 7 checks passed
LucasIcarus added a commit that referenced this pull request Aug 12, 2026
rebase onto 最新 master(f11c0c90,含 #782 vc-agent 沙盒改用户 opt-in)
后,ensureVcMeetingReceiverSession 内 4 个 Session.sandbox* 写点的赋值
右值由硬编码 true 改为跟随 bot 配置 opt-in,归一化 AST 指纹随之漂移。
写点数量、位置与分类(session_owned_persisted / current-session-row)
均未变,本提交仅重新入册指纹并重钉 coverage 台账摘要。

Claude-Session: https://claude.ai/code/session_01AjYRKcQeHGHFxWjKjRJsP6
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