跳到主要内容

GenBI:仪表盘生成与一键部署

30 秒导读: 这一章讲 WrenAI 三个节拍里的第二拍 —— Deploy。当 agent 已经把一个业务问题变成一条受治理的 SQL 答案后,怎么把这个"答案"变成一个可分享、能在浏览器里跑、无需后端的仪表盘应用?核心答案是:wren genbi 这套 CLI 自己不写前端代码,它把"用本项目的上下文层构建一个 GenBI app"合成为一份指令,交给外面的编码 agent 去实现;然后负责登记、部署前防泄密校验、一键部署到用户自己的 Vercel / Cloudflare Pages 账号。跑起来的应用由 wren-core-wasm(语义引擎的 WebAssembly 版)驱动,在浏览器里按 MDL 语义直接查询数据。

本章只覆盖 core/wren/src/wren/genbi/ 这一个子系统。上下文层(MDL、项目文件)怎么来,见 上下文层:MDL 语义模型与项目文件;模型名 SQL 如何被翻译成受治理的物理 SQL,见 语义引擎:模型名 SQL → 受治理物理 SQL


1. 这是什么(零基础也能懂)

一句话定义: wren genbi 是一套把"一次数据分析答案"变成"一个静态仪表盘网站"并帮你部署上线的命令行工具链。

它和你想象的"AI 生成前端"不一样。 最容易误解的一点:Wren 不自己生成前端代码。它扮演的是"包工头 + 质检 + 物流",不是"泥瓦匠"。真正动手写 HTML/JS 的是外部的编码 agent(比如 Claude Code、Cursor 这类)。Wren 做三件事:

  1. 合成一份施工图纸 —— 把静态模板、本项目的实时事实(有哪些模型、哪些列、数据源是什么)、用户的原始需求拼成一份权威的"构建指令",打印到 stdout,交给 agent。
  2. 登记与质检 —— agent 把应用写进 apps/<name>/ 后,Wren 把它登记进项目清单,并在部署前做一次结构校验 + 防泄密扫描
  3. 一键物流 —— 校验通过后,把这个静态文件夹上传到用户自己的 Vercel 或 Cloudflare Pages 账号。

给谁用、解决什么问题: 假设你是一个数据 agent 的搭建者。agent 已经能回答"上季度各区域营收是多少"。现在你想让这个答案变成一个同事点开链接就能看、还能自己改筛选条件的看板 —— 而且你不想为它运维一个后端服务。wren genbi 就是把这最后一公里(从答案到可分享的应用)标准化、并守住"公开站点绝不能泄露数据库密码"这条红线。

为什么能没有后端? 因为语义引擎被编译成了 WebAssembly(wren-core-wasm)。浏览器直接加载这个 wasm 引擎 + 项目的 MDL,就能在客户端按语义模型跑查询 —— 数据要么打包进应用一起发(快照模式),要么由应用连回用户自己的数据源(实时模式)。

用起来什么样: 一条端到端的命令序列大致长这样:

# 1. 让 Wren 吐出一份"构建指令",喂给你的编码 agent
wren genbi build sales-dash --prompt "各区域季度营收看板" --data-mode snapshot

# 2. (agent 按指令把应用写进 apps/sales-dash/ 后)登记进项目清单
wren genbi register sales-dash --data-mode snapshot

# 3. 部署前质检:文件齐不齐、MDL 能不能解析、有没有把密码写进代码
wren genbi verify sales-dash

# 4. 本地预览
wren genbi open sales-dash

# 5. 一键部署到你自己的 Vercel 账号
wren genbi deploy sales-dash --provider vercel

一句话直觉:wren genbi 当成一个发布流水线:build(出图纸)→ agent 施工 → register(入册)→ verify(质检卡口)→ deploy(上线)。Wren 掌管的是"图纸、册子、质检、物流",施工交给 agent,运行时交给浏览器里的 wasm 引擎。


2. 顶层全景(它大概怎么转)

这节讲什么: 把 genbi 子系统的部件、它们的职责、以及一条完整的"从需求到上线"数据流串一遍。

2.1 一张流水线图

下面这张图从左到右就是一次完整的生命周期。关键:Wren(CLI)和 agent 是两个不同的执行者 —— 虚线框内是 Wren 管的,[AGENT] 那一步是外部 agent 干的。

用户需求 + --data-mode


┌─────────────────────────────────────────────────────────┐
│ wren genbi build │
│ ├─ 隐式编译 MDL(若 target/mdl.json 缺失) │
│ ├─ 读项目事实:模型清单 + 数据源 │
│ └─ compose_build_instruction() → 打印"构建指令"到 stdout │
└─────────────────────────────────────────────────────────┘
│ 指令(markdown 纯文本)

[AGENT] 按指令把静态应用写进 apps/<name>/
│ (index.html + mdl.json + 数据资产/连接配置)

┌──────────────────┐ ┌──────────────────────────────┐
│ wren genbi │ │ wren genbi verify │
│ register │─────▶│ ① 必需文件在? │
│ 写 .wren/apps.yml│ │ ② mdl.json 能解析? │
│ status=scaffolded│ │ ③ snapshot 有数据资产? │
└──────────────────┘ │ ④ _scan_for_secrets 防泄密 │
└──────────────────────────────┘
│ 通过 → status=built

┌──────────────────────────────┐
│ wren genbi deploy │
│ ├─ 再跑一遍 verify(卡口) │
│ ├─ resolve_token(Vercel/CF) │
│ └─ provider.deploy() 上传 │
└──────────────────────────────┘
│ 成功 → status=deployed

公开 URL(*.vercel.app / *.pages.dev)


浏览器加载 wren-core-wasm + MDL,
客户端按 MDL 语义查询(无后端)

2.2 部件一句话职责

部件干什么文件
composer把模板 + 项目事实 + 用户需求合成为"构建指令"genbi/composer.py
index维护项目内的 GenBI app 清单(.wren/apps.yml)genbi/index.py
verify部署前结构校验 + 防泄密扫描genbi/verify.py
tokens三级查找 Vercel/Cloudflare 部署令牌genbi/tokens.py
cliTyper 子命令,把 compose→verify→deploy 串起来genbi/cli.py
providers可互换的部署适配器(Vercel REST / Cloudflare wrangler)genbi/providers/
wren-core-wasm浏览器端的语义引擎,让部署产物无需后端core/wren-core-wasm/(外部)

2.3 主线走一遍(高层)

一条最短路径:需求进来 → build 出图纸 → agent 施工 → register 入册 → verify/deploy 质检并上线 → 浏览器里 wasm 跑起来。

贯穿全程有一条暗线:Wren 反复强调"这是个公开静态站点,任何人打开 URL 都能读到每一个文件"。这条暗线塑造了几乎所有关键设计 —— --data-mode 的两种取舍、verify 的默认拒绝式防泄密扫描、令牌绝不走命令行参数。理解了"产物是公开的",就理解了这个子系统一半的设计动机。


3. 核心原理(逐个机制,由浅入深)

3.1 构建指令合成:Wren 不写前端,而是"发图纸"

它要解决的小问题: 怎么让一个通用编码 agent,构建出一个正确挂载了本项目上下文层的 GenBI 应用?

思路: agent 很会写前端,但它不知道"你这个项目有哪些模型、哪些列、数据源是什么、wasm 引擎该怎么接线"。Wren 的价值就是把这些项目专属的事实注入进一份指令里,让 agent 拿到的不是空白提示,而是一份填好了项目上下文的施工图纸。

这个设计和 Wren 别处的 wren context instructions 是同一个模式:CLI 不执行动作,只产出一份权威的、纯 markdown 的指令交给 agent(见 composer.py 模块 docstring)。

指令里有什么: compose_build_instruction 把五类东西拼进一份 markdown(composer.py:65 compose_build_instruction):

区块内容来源
wasm 接线从 CDN 加载 wren-core-wasmloadMDLquery 的样板静态模板 + 版本常量
项目上下文编译好的 MDL 路径、目标文件夹、数据源、数据模式CLI 传入
可用模型每个模型及其列名的清单load_models()(见 02)
数据处理指引data_mode 二选一的具体步骤_data_mode_guidance
用户需求用户原始 prompt,逐字附在末尾CLI 传入

版本钉死。 wasm 引擎的版本用一个常量钉住,并要求和 wasm 包的 package.json 保持同步(composer.py:15 WREN_CORE_WASM_VERSION = "0.4.1")。指令里明确让 agent 从共享 CDN 加载,不要把 ~68MB 的 wasm 二进制打进应用文件夹(composer.py:83-96)。

模型清单怎么格式化。 一个小而实用的函数,把模型列表变成每行一个 bullet、附上列名;模型为空时给出可执行的补救提示(composer.py:54 _format_model_inventory):

# 真实源码 composer.py:54-62 _format_model_inventory
def _format_model_inventory(models: list[dict]) -> str:
if not models:
return "- (no models found — run `wren context build` first)"
lines = []
for model in models:
cols = ", ".join(c.get("name", "?") for c in model.get("columns", []))
lines.append(f"- **{model.get('name', '?')}**: {cols}")
return "\n".join(lines)

这段做的事:把每个模型渲染成 - **模型名**: 列1, 列2, ...;找不到模型时不是报错,而是给 agent 一句可照做的提示。

数据模式:嵌入数据 vs 实时查询

这是 build 阶段最重要的一个决策。 --data-mode 决定了部署产物如何拿到数据,只有两个合法值(composer.py:17 DATA_MODES = ("snapshot", "live")),_data_mode_guidance 按它派发不同的施工指引(composer.py:46)。

维度snapshot(快照 / 嵌入数据)live(实时查询)
数据来源打包进应用的静态资产(*.parquet / *.duckdb)部署后连回用户自己的数据仓库/API
后端完全无后端,纯静态应用里写一个"端点 URL"连接配置
引擎怎么查浏览器里 wasm 直接查静态资产wasm 通过端点查实时数据
关键红线——数据库凭证绝不能内联进应用
额外要求把数据导出为 parquet 放进 data/端点须对部署域名开放 CORS

live 模式的指引里有一条硬规则,值得原文体会(composer.py:24-30):

"HARD RULE: warehouse credentials MUST NEVER be inlined into the app. The app is a public static site..."

指引明确说:wren genbi verify 的密钥扫描只是"尽力而为的纵深防御",不是保证 —— 真正守住秘密的是"凭证绝不内联"这条硬规则本身(composer.py:26-30)。用代理/带鉴权的 API,或浏览器端鉴权,而不是把密码写进公开文件。

未知模式直接抛错。 _data_mode_guidance 对不认识的模式 raise ValueError(composer.py:51),配合 CLI 层的前置校验(cli.py:113),让一个拼错的 --data-mode(比如 snapsho)在早期就失败,而不是默默产出错误指引。


3.2 应用登记与生命周期:项目内的 app 清单

它要解决的小问题: 一个项目里可能有多个 GenBI app,Wren 怎么知道有哪些、各自什么状态、部署到哪了?

思路: 用一个机器写、机器读的清单文件当唯一事实源 —— <project>/.wren/apps.yml(index.py:18 _INDEX_RELPATH,index.py:24 index_path)。它刻意模仿了 ~/.wren/profiles.yml 的注册表模式,并且只存非机密的链接状态,秘密永远不进这里(见 index.py 模块 docstring)。

生命周期是个三态状态机(index.py:21 STATUSES):

register verify 通过 deploy 成功
scaffolded ─────────▶ (built) ─────────▶ deployed
刚登记 质检过关 已上线
  • register_app 首次登记时置为 scaffolded(index.py:80)。
  • verify 通过后,若还停在 scaffolded 才推进到 built(cli.py:266-267)。
  • deploy 成功后置为 deployed,并把部署链接状态一起写回(cli.py:373)。

清单的 CRUD 由五个纯函数承担:

函数作用位置
load_index读清单,不存在则返回空骨架index.py:32
save_index写回 YAML(不排序键,保持人可读顺序)index.py:74
register_app新建或更新某 app 的条目index.py:80
remove_app删条目(返回 False 表示本就没登记)index.py:94
get_app取单个条目或 Noneindex.py:104
update_app把若干字段合并进条目并持久化index.py:109

防御式读取是这里的精华。 load_index 花了很大篇幅处理"文件存在但坏了"的情况,因为一个坏清单如果不早点报错,会在某个命令深处炸出一个看不懂的 AttributeError。它的处理分层很清楚:

# 真实源码 index.py:42-52(节选)load_index
try:
data = yaml.safe_load(path.read_text())
except yaml.YAMLError as e:
raise MalformedIndexError(f"{path} is not valid YAML: {e}") from e
if data is None:
data = {}
if not isinstance(data, dict):
raise MalformedIndexError(
f"{path} is malformed — expected a mapping at the top level, "
f"got {type(data).__name__}."
)

三层校验依次是:能不能解析成 YAML → 是不是 mapping → apps 键是不是 mapping。任何一层不过都抛 MalformedIndexError 并带上出问题的路径(index.py:28 MalformedIndexError)。

一个真实踩过的坑藏在注释里(index.py:53-59):早先用 setdefault 规整 apps: 这种"键在但值为 null"的情况 —— 但 setdefault 对已存在的键是 no-op,于是 apps: None 被原样留下,让后面每个访问器都崩。修法是显式判 is None(而不是判真假),这样一个合法的 falsy 值仍会被保留、交给校验去暴露,而不是被悄悄改成默认值(index.py:60-69)。


3.3 交付前校验:结构检查 + 防泄密扫描

它要解决的小问题: 怎么保证一个 agent 写出来的应用"能跑"、并且"不会把公司数据库密码发到公网上"?

思路: 在产物走向公开 URL 之前,设一道确定性的、无浏览器的卡口(verify.py 模块 docstring)。它只做静态结构检查,不真的启动浏览器跑 wasm(那是未来可能的加固)。

verify_app 的检查项依次是(verify.py:111):

  1. 数据模式合法 —— 未知模式直接 fail closed,避免拼错的模式绕过后续检查(verify.py:119-126)。
  2. 应用文件夹存在 且有 index.html 入口(verify.py:128-132)。
  3. mdl.json 存在、非空、是合法 JSON(verify.py:134-143)。
  4. 快照模式必须有数据资产(*.parquet / *.duckdb),否则应用没数据可查(verify.py:145-155,verify.py:15 _DATA_ASSET_SUFFIXES)。
  5. 每种模式都跑防泄密扫描(verify.py:158)。

结果用一个小 dataclass 承载(verify.py:105 VerifyResult):passed 布尔 + failures 消息列表。

_scan_for_secrets:默认拒绝式的密钥扫描

这是本子系统最能体现"公开产物"暗线的地方(verify.py:68 _scan_for_secrets)。它的核心设计决策是 默认扫描一切,只跳过明确的二进制/数据格式,而不是"只扫某几类文本文件":

为什么是默认拒绝(default-deny)? 注释讲得很直白(verify.py:17-22):如果用"允许名单"(只扫 .html/.js…),你永远追不上 agent 下一次会用什么扩展名(.env.ts.vue…)—— 而这些恰恰是最可能藏着凭证、又都会被发到公开站点的文件。

所以它反过来:遍历每个普通文件,只跳过已知的二进制/数据后缀(verify.py:23 _UNSCANNABLE_SUFFIXES,含 .parquet/.wasm/.png/.woff2/.zip 等),其余一律读文本、跑正则。

扫描有两条独立的防线:

防线一:.env* 文件仅凭存在就判失败。 不管里面内容能不能被下面的正则识别 —— 一个 .env 文件出现在公开静态应用里几乎必然是错误,而 wrangler pages deploy . 会把它整个发出去,所以这道门是唯一能拦住它的(verify.py:82-88)。

防线二:窄正则匹配内联凭证(verify.py:48 _SECRET_PATTERNS),模式刻意保持窄以避免误报:

模式抓什么
connection string with passwordscheme://user:pass@host 这种带密码的连接串
AWS access key idAKIA 开头的 AWS key
password/secret/token assignmentpassword = "..." / api_key: "..." 这类赋值(值 ≥8 字符)
# 真实源码 verify.py:76-88(节选)_scan_for_secrets
for path in sorted(app_dir.rglob("*")):
if not path.is_file() or path.is_symlink():
continue
if path.name.startswith(".env"): # 防线一:.env 仅凭存在即失败
failures.append(...)
continue
if path.suffix.lower() in _UNSCANNABLE_SUFFIXES: # 跳过二进制/数据
continue

这段重点看两点:符号链接被直接跳过(providers 也不发它们),以及 .env 的处理排在后缀过滤之前 —— 保证它绝不会因为"后缀可疑"之类理由被漏掉。


3.4 令牌解析:密钥绝不走命令行

它要解决的小问题: 部署要用 Vercel / Cloudflare 的 API 令牌,怎么拿到它、又不让它泄露?

思路: 令牌是秘密,而命令行参数会漏进 shell 历史和进程列表(ps 能看到)。所以 Wren 抄了 Vercel agent-skill 的模式:令牌永远不接受 --token 这种 CLI flag,而是从环境里发现(tokens.py 模块 docstring)。

resolve_token 做三级查找(tokens.py:14):

① 进程环境变量(shell export 的最优先)
│ 命中即返回

② 标准 .env 发现(cwd → 向上走到项目根 → ~/.wren/.env)
│ 命中即返回

③ 显式 --path 项目目录下的 .env(覆盖 cwd 之外的项目)


都没有 → 返回 None(由调用方决定是否提示)

三级依次是:shell 环境 > 通用 .env 发现 > 指定项目的 .env。找不到不报错,而是返回 None,把"要不要提示用户"的决定权留给调用方(tokens.py:41)—— deploy 命令拿到 None 时会打印一条可操作的提示,并再次强调"绝不要用 CLI flag 传令牌"(cli.py:342-350)。

每个 provider 声明自己要读哪个环境变量:Vercel 读 VERCEL_TOKEN(vercel.py:55),Cloudflare 读 CLOUDFLARE_API_TOKEN(cloudflare.py:47)。


3.5 CLI 编排:把 compose → verify → deploy 串起来

这节讲什么: genbi/cli.py 是把前面所有部件粘起来的 Typer 子应用(cli.py:10 genbi_app)。这里讲两个最值得看的点:路径安全 和 deploy 的完整编排。

应用名即路径:一道防目录穿越的关卡

app 名字会变成 <project>/apps/ 下的一段路径。一个精心构造的名字(比如 ../../etc)如果不拦,就能逃出 apps/_resolve_app_dir 用一条 slug 正则堵死这个口子(cli.py:18 _APP_NAME_RE,cli.py:21 _resolve_app_dir):

# 真实源码 cli.py:18 _APP_NAME_RE
_APP_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")

它要求名字以字母数字开头,只含字母数字、_- —— 于是路径分隔符、..、以点开头的名字全被拒,返回的路径永远被关在 <project>/apps/ 里面build/register/verify/open/deploy 全走这个函数。

deploy:一次完整编排

deploy 命令(cli.py:298)是整条流水线的收口,步骤环环相扣:

deploy <name> --provider vercel [--prod]

├─ 1. _require_registered:没登记就报错退出 (cli.py:321)
├─ 2. get_provider(provider):取适配器,未知 provider 报错 (cli.py:325)
├─ 3. verify_app:再跑一遍质检 —— "坏应用永不落到公开 URL" (cli.py:331)
├─ 4. resolve_token:三级找令牌,找不到给可操作提示 (cli.py:341)
├─ 5. adapter.deploy(...):真正上传,失败抛 DeployError (cli.py:352)
└─ 6. update_app:status=deployed + 写回 deploy 链接状态 (cli.py:373)

注意第 3 步:部署前会再独立跑一次 verify(不是复用 verify 命令的结果),注释点明动机是"a broken app never reaches a public URL"(cli.py:330)。这是"公开产物"暗线的又一次体现 —— 质检是部署自己的一部分,不能靠用户记得先跑 verify

其余命令各司其职:

命令作用位置
build打印构建指令(必要时先隐式编译 MDL)cli.py:63
register登记 agent 写好的应用cli.py:159
list列出已登记的应用及状态、部署 URLcli.py:195
remove从清单删除(保留 apps/<name>/ 文件)cli.py:215
verify单独跑质检cli.py:244
open本地起 HTTP server 预览cli.py:271
deploy质检 → 部署cli.py:298

build 的一个体贴细节: 如果 target/mdl.json 还没编译,build隐式先编译一次(cli.py:123-134),这样喂给 agent 的指令里永远是最新的上下文层,而不是过期或缺失的 MDL。


3.6 部署适配器:Vercel 走 REST,Cloudflare 走 wrangler

它要解决的小问题: 把一个静态文件夹上传到不同的托管商,它们的上传方式差别很大,怎么用统一接口包住?

思路: 定义一个 DeployProvider 协议(providers/base.py:25),每个托管商写一个适配器实现它;get_provider(name) 按名字返回适配器,未知名字抛 KeyError 并列出已知集合(providers/__init__.py:8)。返回值统一是一个 Deployment dataclass —— 只含非机密的链接状态(URL、环境、项目/账号 id)(providers/base.py:10 Deployment)。

两个适配器走了截然不同的路,原因很实际:

维度Vercel(vercel.py)Cloudflare Pages(cloudflare.py)
上传方式v13 REST API,文件 base64 内联,一次调用搞定shell out 到 wrangler pages deploy
为什么有单次内联上传端点,无需 CLIPages 没有单次内联 REST 端点;官方建议就是用 wrangler(cloudflare.py:1-12)
令牌怎么传只在 Authorization header 里,绝不进 argv只经子进程环境变量,绝不进 argv
额外要求——需要 wrangler(或 npx wrangler)+ CLOUDFLARE_ACCOUNT_ID
URL 怎么拿从 API 响应读用正则从 wrangler 输出里抓 *.pages.dev(cloudflare.py:22)

两处共同的安全细节都围着"公开产物"暗线转:

  • Vercel 收集文件时跳过符号链接、跳过解析后落在 build_dir 之外的路径、跳过 .env*(vercel.py:28-50 _collect_files)—— 哪怕 verify 漏了,这里再拦一道,防止一个野生 symlink 把磁盘上别处的文件也发出去。
  • Cloudflare 部署时用 cwd=build_dirdeploy .,这样 wrangler 不会误捡项目根里某个无关的 wrangler.toml(cloudflare.py:100-117)。

令牌传递方式再强调一遍(两个适配器 docstring 都写了):Vercel 令牌只走 Authorization header(vercel.py:5-6),Cloudflare 令牌只走子进程环境(cloudflare.py:79-84)—— 都绝不进命令行参数。


4. 浏览器端的运行时:wren-core-wasm 让产物无需后端

这节讲什么: 前面都在讲"怎么造和发",这节讲"发出去的东西靠什么跑"。

部署产物是纯静态站点,却能按语义模型查询数据 —— 秘密在于语义引擎被编译成了 WebAssembly。README 把这一拍描述为(README:56):

"Deploy. Turn any answer into a shareable, browser-side dashboard powered by wren-core-wasm..."

wren-core-wasm 是 Rust 语义引擎 wren-core 的浏览器构建,以 npm 包分发,产物约 68MB(见根 CLAUDE.md 的 wasm 段)。构建指令让 agent 从 CDN(unpkg)加载它、而不是打包进应用(composer.py:83-96),典型接线是:

// 示意,非源码 —— 摘自 composer.py 生成的指令模板
import { WrenEngine } from
"https://unpkg.com/@wrenai/wren-core-wasm@0.4.1/dist/index.js";
const engine = await WrenEngine.init();
await engine.loadMDL(mdl, profile); // profile: { source: "<url前缀或空>" }
const result = await engine.query(sql); // 按 MDL 语义查询

这就是"无后端"的关键闭环: 浏览器加载 wasm 引擎 + 项目的 mdl.json,profile.source 指向静态数据资产(快照模式)或实时端点(实时模式)。查询在客户端按 MDL 语义解析、执行 —— 和服务端语义引擎走的是同一套 MDL 治理(见 03),只是搬进了浏览器。所以一次 wren genbi deploy 之后,分享出去的就是一个自带语义引擎、任何人打开即用、不需要你运维任何服务的仪表盘。


5. 巧妙之处(可借鉴的技术)

"CLI 不做,只发指令"这个反直觉的分工。 Wren 明明是做数据的,却坚决不生成前端,而是把项目事实注入一份指令交给通用 agent(composer.py:65)。妙在分离了"项目专属知识"和"通用编码能力" —— Wren 只负责它独有的部分(上下文层),把它不擅长也没必要做的前端施工外包出去。

"公开产物"作为贯穿全局的第一性原理。--data-mode 的硬规则(composer.py:24),到 default-deny 的密钥扫描(verify.py:17-22),到 deploy 内置的强制质检(cli.py:330),到 provider 里跳过 symlink/.env(vercel.py:28-50)—— 同一条"任何人能读到每个文件"的暗线,推导出了一整套一致的防御。值得学的是从一个物理事实反复推导设计,而不是零散打补丁。

纵深防御,且诚实标注每层的能力边界。 密钥防护有三层:硬规则(靠人)、verify 扫描(尽力而为)、provider 再过滤。而且代码明确写出扫描"不是保证"(composer.py:26-30)—— 不夸大自动化能力,把最终责任说清楚。

default-deny 胜过 allowlist。 密钥扫描默认扫一切、只跳过二进制(verify.py:17-22),背后的洞察是"你追不上 agent 下一个会用的扩展名"。这个"面对不可预测的输入,反过来枚举安全的例外"的思路,在安全设计里很通用。

令牌永不进 argv。 三级环境查找(tokens.py:14)+ header/子进程环境传递(vercel.py:74cloudflare.py:79-84),系统性地把秘密挡在 shell 历史和进程列表之外。

防御式读清单 + 注释留证。 load_index 分层校验坏文件(index.py:42-69),并把"setdefault 曾导致 apps: None 崩溃"这个真实踩坑写进注释(index.py:53-59)—— 让后人知道"为什么这里不能图省事"。


6. 边界与局限

verify 不是真的把应用跑起来。 它只做静态结构检查,不启动浏览器跑 wasm 冒烟查询;模块 docstring 自己承认"真正的 headless wasm 冒烟查询是未来可能的加固"(verify.py:1-6)。所以"文件齐、MDL 能解析"不等于"查询真能出结果"。

密钥扫描按设计会漏。 正则刻意保持窄以免误报(verify.py:47),default-deny 也只是缩小而非消灭风险面。代码反复强调它是纵深防御、不是保证(composer.py:26-30)—— 真正的保障是"凭证绝不内联"的人为纪律。

只支持两种数据模式、两个部署商。 DATA_MODES 只有 snapshot/live(composer.py:17);provider 注册表只有 vercel/cloudflare(providers/__init__.py:13-16),base 里提到"wren SaaS later"是未来项(providers/base.py:1)。

Cloudflare 部署依赖外部 wrangler CLI。 没有 wrangler 也没有 npx 就直接失败并提示安装(cloudflare.py:71-77);URL 是从 wrangler 文本输出里正则抓的(cloudflare.py:125),若上游改了输出格式可能抓不到。

live 模式的 CORS 要用户自己配。 端点必须对部署域名开放 CORS,这条 Wren 只能提醒、无法代办(composer.py:31-32)。

清单只记链接状态,不备份应用。 remove 只删清单条目,apps/<name>/ 的文件原样保留(cli.py:227);清单本身不做版本或并发保护。


7. 横向对比(同组其它章)

genbi 是 WrenAI 这个数据 agent 货架上"最后一公里"的部件,它站在前几章的产出之上:

关切本章(genbi)相邻章
上下文从哪来消费编译好的 MDL + 模型清单02 上下文层:MDL 语义模型与项目文件
查询语义如何治理部署产物在浏览器里用同一套 MDL 语义03 语义引擎:模型名 SQL → 受治理物理 SQL
CLI/skills 如何下发genbi 是 wren CLI 的一个子命令组01 Agent 驱动的 CLI 与 skills 下发
过往查询如何召回(不直接相关;答案可来自召回)04 记忆与检索
全景与阅读顺序——index

一句话定位:02/03 把"正确的答案"造出来,genbi 把这个答案"打包上线成可分享的应用"。


8. 代码地图(导航索引)

用符号名 grep 比行号更抗漂移。下面每行指向一个该打开的真实符号。

主题文件路径符号名
合成构建指令(总入口)core/wren/src/wren/genbi/composer.pycompose_build_instruction
数据模式派发指引core/wren/src/wren/genbi/composer.py_data_mode_guidance / _LIVE_GUIDANCE / _SNAPSHOT_GUIDANCE
模型清单格式化core/wren/src/wren/genbi/composer.py_format_model_inventory
wasm 版本 / 数据模式常量core/wren/src/wren/genbi/composer.pyWREN_CORE_WASM_VERSION / DATA_MODES
清单路径 / schema 版本 / 状态机core/wren/src/wren/genbi/index.pyindex_path / INDEX_SCHEMA_VERSION / STATUSES
防御式读清单core/wren/src/wren/genbi/index.pyload_index / MalformedIndexError
清单 CRUDcore/wren/src/wren/genbi/index.pyregister_app / remove_app / get_app / update_app / save_index
结构校验(总入口)core/wren/src/wren/genbi/verify.pyverify_app / VerifyResult
防泄密扫描core/wren/src/wren/genbi/verify.py_scan_for_secrets / _SECRET_PATTERNS / _UNSCANNABLE_SUFFIXES
令牌三级解析core/wren/src/wren/genbi/tokens.pyresolve_token
CLI 子命令core/wren/src/wren/genbi/cli.pybuild / register / list_apps / remove / verify / open_app / deploy
应用名 slug 防穿越core/wren/src/wren/genbi/cli.py_APP_NAME_RE / _resolve_app_dir
部署适配器协议core/wren/src/wren/genbi/providers/base.pyDeployProvider / Deployment / DeployError
适配器注册表core/wren/src/wren/genbi/providers/__init__.pyget_provider
Vercel 适配器(REST)core/wren/src/wren/genbi/providers/vercel.pyVercelProvider / _collect_files
Cloudflare 适配器(wrangler)core/wren/src/wren/genbi/providers/cloudflare.pyCloudflareProvider / _wrangler_cmd
浏览器端语义引擎(外部)core/wren-core-wasm/WrenEngine(npm wren-core-wasm)