From 239e27cceef848408af3da2ee92be532ea856269 Mon Sep 17 00:00:00 2001 From: xu4wang Date: Wed, 8 Jul 2026 10:55:06 +0700 Subject: [PATCH] =?UTF-8?q?fix(read-isolation):=20botmux=20quoted=20?= =?UTF-8?q?=E5=9C=A8=E9=9A=94=E7=A6=BB=20bot=20=E4=B8=8B=E5=8F=AF=E7=94=A8?= =?UTF-8?q?=20+=20=E4=B8=8B=E8=BD=BD=E8=A2=AB=E5=BC=95=E7=94=A8=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 隔离 bot 的 sandbox 里 bots.json 被 read-isolation deny(含所有 bot 密钥,必须 deny), 导致两个问题: ① `botmux quoted` 走 resolveSessionAppId→loadBotConfigs() 读 bots.json→被 deny→注册静默 失败→getMessageDetail 抛 "Bot not registered"。cmdHistory/cmdSend 早有 send-cred 兜过, quoted 漏了。→ 开头补 `registerSelfFromCredFile()`(用 BOTMUX_LARK_APP_ID + 自己 BOT_HOME 的 send-cred.json 注册,不读 bots.json),与 history/send 一致。 ② 即便拿到被引用消息,其文件资源只是 key+name;隔离 bot 无凭证自己调 Lark resource API 下载。 → 渲染后对 rendered.resources 调 daemon 同款 downloadResources,用已注册 client(tenant token) 把字节下到自己 appId 桶 attachments///(carve-out 可读;sandbox 只 deny 读、 不 deny 写),并把 attachments[].path 塞进输出,agent 直接读路径即可。 仅动 src/cli.ts 的 cmdQuoted。依赖本分支(#387)引入的 registerSelfFromCredFile / downloadResources / appId 分桶,故 stack 在 #387 之上。验证:codex review 两轮零 findings + 用户 live 实测隔离 bot 引用回复文件、quoted 输出含本地 path 并成功读取。 Co-Authored-By: Claude Opus 4.8 --- src/cli.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/cli.ts b/src/cli.ts index 8ae7e07d..f460ee48 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4320,6 +4320,11 @@ async function cmdQuoted(rest: string[]): Promise { process.exit(1); } + // Read isolation: register this bot from its own send-cred file so the Lark + // client (getMessageDetail below) is available WITHOUT reading the denied + // bots.json — same as cmdHistory / cmdSend. Missing this was why a sandboxed + // isolated bot's `botmux quoted` failed "Bot not registered". + await registerSelfFromCredFile(); const { larkAppId: appId } = await resolveSessionAppId(sessionIdArg); const { getMessageDetail } = await import('./im/lark/client.js'); @@ -4343,6 +4348,19 @@ async function cmdQuoted(rest: string[]): Promise { const merged = await resolveMergedCardContent(appId, messageId).catch(() => null); if (merged) rendered.content = merged.text; } + // The referenced message's file/media resources arrive as key+name only. A + // read-isolated agent can't call the Lark resource API itself (bots.json + // creds are deny-read), so download the bytes HERE — via the bot client + // registered above — into this bot's OWN attachment bucket + // (attachments///, read-allowed by its carve-out; sandbox + // denies file *reads*, not writes). Surface the local paths so the agent can + // actually open the file instead of only seeing its key. + if (rendered.resources?.length) { + const { downloadResources } = await import('./core/session-manager.js'); + const { attachments, needLogin } = await downloadResources(appId, messageId, rendered.resources); + (rendered as { attachments?: unknown }).attachments = attachments; + if (needLogin) (rendered as { needLogin?: boolean }).needLogin = true; + } console.log(JSON.stringify(rendered, null, 2)); } catch (err: any) { console.error(`获取被引用消息失败: ${err.message}`);