Z5(c2e413e)把 web-fetch 的降级链与注入点做完了,但「取正文」入口缺失 ⇒ 组合根拿不到 BodyRenderer、这一档实际关着(当时如实登记)。本片补上: - internal/capability/browser/render.go:新增 Manager.RenderText(ctx,url) (导出的进程内能力)—— 先过 GuardURL(与全仓同一份 SSRF 判定源)、复用 withSession/session、等 readyState=complete 之后才取 body.innerText; 渲染失败 = 分类错误,渲染成功但正文为空 = 空串+无错(不冒充成功/失败)。 - cmd/cyrene/wire_body_fallback.go:适配器 + 挂载点,把 Manager 适配成 io.BodyRenderer 并用 WithBrowserFallback 挂上;web_runtime 里把浏览器 接线**上移到 io 工具集之前**(顺序颠倒会静默关死这一档)。 - plugins/tools/browser/tool.go:新增 Tool.Manager(),供组合根取驱动句柄。 开关口径(裁决,依据 design/28 §1 M5 / §7 判据 4 / §8 B5):**有可用浏览器 就挂,没有就不挂**,不新增开关 —— ① 设计把 M5/B5 写成要落地生效的降级档,不是永远关着的空壳; ② 设计 §8 的可调项清单里没有这条降级档的开关,`.env` 已冻结 ⇒ 自造开关属 「自行发挥」; ③ io 库层「不传 = 默认关」的语义一字未改,本片只把组合根从「永远关」改成 「没来源就关」,并留带来源的 WARN(reason=no-usable-browser)。 ⚠️ 若日后要「有浏览器也需显式开启」,得先给设计定开关落点,代码侧一行改回。 验收(主会话复跑 + 亲自变异):go build ./... + 三包 vet 绿;browser/io 两包 测试绿(cmd/cyrene 由子代理跑过全绿);我用 -overlay 把 GuardURL 的判定对象 换成常量 URL(绕过被拒目标)⇒ TestManager_RenderTextBlockedURLRejectedBeforeAnyProcess 变红,证明判定确实**在起任何进程之前**跑。 ⚠️ 生产端到端未验:按红线没重启后端、没起真 Chrome ⇒「真页面 → browser 取到 正文」这条链只有假 CDP + 组合根接线级证据。 ⚠️ design/28 有 6 处表述与代码现状冲突(归属 web-fetch vs web-search、extract 动作归属、B5 排期、状态过期等),已登记待我同步文档。