本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
模块:T3 解剖 OpenAI Codex:把安全观写进类型系统
来源:xueai.miyang.cn(小山学堂 · 洛小山)
本集为三节合辑(素材 12144 字),已做取舍,只保留最能带走的部分。
模型按 token 往外推,终端却是一个写出去就改不了的字符网格。已经不会变的行交给 scrollback,还可能变的尾巴留在活动 cell,表格没闭合之前整段扣住。
统一主线:三节都在处理同一件事 —— 边界划在哪里。
| 节 | 边界 | 划错的后果 |
|---|---|---|
| 终端两区 | 可变 vs 不可变 | 列宽抖动、残影 |
| 架构决策 | 人读 vs 机器查 | 文字还在,对象已搬家 |
| 安全视角 | 事前 vs 事后 | 能回放但没能拦住 |
| 区域 | 归属 | 能否再改 |
|---|---|---|
| 稳定区(scrollback) | 交给终端,进程内无副本 | 不可改,提交即固化 |
| 活动尾巴(active cell) | 留在进程内 | 下一帧整段可换 |
三个指针同向移动:enqueued_stable_len(已入队行数)→ emitted_stable_len(已写入 scrollback 行数);活动尾巴从 enqueue 边界算起,不从 emit 边界算起 —— 否则排队未写出的行会在活动区重复出现。
出处:codex-rs/tui/src/streaming/controller.rs L1–36。
| 状态 | 触发 | 尾巴预算 |
|---|---|---|
None | 普通散文 | 0,行直接进稳定队列 |
PendingHeader | 上一行像表头、下一行未到 | 从 header 起整段扣住 |
Confirmed | 表头 + delimiter 两行对上 | 从 header 起整段扣住,直到 finalize |
Raw 模式 | 关闭扣留 | 0,表格当纯文本流走 |
出处:table_holdback.rs L21–32;controller.rs L384–412。
| 规则 | 检查方式 | 失效表现 |
|---|---|---|
| 调用点参数注释 | Dylint 插件(当一次 rustc) | 合并前亮红 |
| 依赖锁文件 | CI | 合并前亮红 |
| Markdown 里的路径 | 无 | 静默失效 |
| 800 行上限 | 无(停在评审) | 静默失效 |
| 门 | 看什么 | 行为 |
|---|---|---|
| 策略 | argv | Allow / Prompt / Forbidden 三态取最严 |
| 审批 | 人 | 精确 key(完整 argv + cwd + 权限) |
| Guardian | 模型判断 | 超时/坏输出关闸 |
| OS 沙箱 | 路径 + syscall | macOS SBPL / Linux bubblewrap+seccomp / Windows 受限令牌 |
| 出站代理(漏斗) | 域名 | 403 + 错误头回给命令进程,循环继续 |
散文一行行往下长没问题;一旦开始吐表格,列宽每来一行就变一次,刚对齐的列被挤到下一列,上一帧的竖线还印在屏幕上。往上滚时历史和尾巴叠在一起。
网页换 DOM 时浏览器会保住滚动位置;终端往 stdout 写一个字,光标就往前走一格,没有回头改的余地。
用户看见的最小时间单位是一行 Markdown source。半行表格先画出来,下一秒结构一对,列会立刻消失再长出来。未结束的 source 进缓冲,不能改可见尾巴。
出处:chatwidget/streaming.rs L489–492。
排查指引:遇到「流式输出卡住」,先问这一帧里有没有换行。没有换行是设计内行为,不是 bug。真正的问题在另一端 —— 模型长时间不吐换行时用户以为界面死了,此时需要的是心跳提示,不是改渲染逻辑。
markdown 表加一行就能改所有列宽。表头若已按窄列冻进 scrollback,后面的长单元格没法回去改它。
扫描器只认 header + delimiter:
PendingHeader,乐观扣住;None,那一行再进稳定队列;Confirmed,从 header 起整张表留尾巴直到 finalize。误伤控制:一句 status | owner | note 看起来像表头,其实只是散文。因此要求两行对上才确认,只有一行时保持待定而非直接判成表。
sh 围栏里的竖线当代码,不触发扣留;为什么长期成立:列宽是全局量,局部追加会改已经画过的行。把整张未闭合的表留在可变区是这条约束的最小解。网页对应做法:表格节点在闭合之前不要拆进不可变 DOM 片段。
| 档位 | 行为 | 进入条件 |
|---|---|---|
| 平滑档 | 每 tick 出一行 | 默认 |
| 追赶档 | 一次抽空队列 | 深度 ≥ 8 行,或最老一行 > 120ms |
| 退出 | 回平滑 | 深度 ≤ 2、年龄 ≤ 40ms,且保持 250ms |
| 再进入保护 | 挡 250ms | 除非堆到 64 行或 300ms |
策略不看这段文本是标题还是表格,只看队列深度和年龄。出处:streaming/chunking.rs L82–125。
历史行用转义序列写到 viewport 上方,界面库再画输入框和活动尾巴。拆成两次刷新,用户会先看见历史往上跳一截,输入框还停在旧位置。
因此 scrollback 插入与 viewport 绘制包进同一次 sync_update;双 buffer 只把变过的格子写出去;画回调必须画满整帧,少画一块终端就留下上一帧残字。出处:tui.rs L954–973。
共同点:不要用平均值对付两种压力,也不要把同一帧的多次写入拆开提交。
同一份 AGENTS.md 里:位置参数少了注释 → 本地命令红;改了 Cargo.toml 忘刷 Bazel 锁 → CI 红;第 35 行的路径 → 没有任何检查器。真实文件叫 connection_manager.rs,文档里那个带 mcp_ 前缀的名字是重命名后的残留。
std / tokio 直接放过;.enabled(false));None、布尔、数字要写,字符串和字符放过。出处:tools/argument-comment-lint/src/lib.rs L165–180、L261–274;CI 三平台各跑一次:.github/workflows/rust-ci.yml L164–187。
首选永远是改 API 让调用点自己能读;改不了 API 才允许 /*param_name*/。lint 是退路,不是首选。
同一份文档第 265 行:写着 v2.rs,当前是目录 v2/,下面拆成三十多个文件。文件靠近 800 行就要拆,拆了之后指南里的单文件路径没人改。
模块行数规则点名五个高频文件,四个已越过 800,一个贴着 900;chat_composer.rs 按行计 12859 行。仓库里没有数行数的命令 —— 行数能数,CI 不数。
路径存在性本是最容易检查的:抽出反引号路径,对仓库根做存在性判断,二十行脚本的事。仓库没做 —— 预算花在调用点可读性上,没花在路径存在性上。
这是典型的取舍失误:难的检查做了,便宜的检查没做。
横向对照:DSH 把「每个包必须拥有 ./invariant」同时写成散文和 21 行 verify-package-invariants(失败 process.exit(1)),空安装器必须带固定前缀 No runtime invariant: —— 空是显式架构结论,不是疏忽。
| 视角 | 问什么 | 依据 |
|---|---|---|
| 可回放 | 出事之后,现场还能不能拼回来 | JSONL 原文 |
| 可拒绝 | 出事之前,有没有一道门能说不 | 四道门 + 出站漏斗 |
thread-store/README.md L22–28。TurnStarted 留下。能回放 turn 边界和完成态,回放不了当时屏幕上闪过的审批文案。出处:rollout/src/policy.rs L86–105。TurnStarted,进行中的 turn 直接拒绝,投影出的合成 ID 不能当切点。出处:thread_rollout_truncation.rs L187–191。Decision 只有 Allow / Prompt / Forbidden,用序取最严;规则文件的 not_match 加载器真跑一遍,反例命中则会话开不起来。出处:execpolicy/src/decision.rs L9–16、rule.rs L281–306。npm run test 与 npm run lint 是两把 key。出处:unified_exec.rs L86–97。guardian/mod.rs L1–12。None。出处:sandboxing/src/manager.rs L36–42。x-proxy-error 头回给命令进程,循环继续。出处:network-proxy/src/responses.rs L76–83。沙箱拒绝走的是成功的工具输出:process_id 被清掉,exit_code 留在正文里,记日志时成功位恒为 true。
若把沙箱拒绝升级成引擎错误,模型看不到退出码,只会换一条更绕的命令。工具层只允许两种失败:回喂模型,或打断引擎。出处:function_call_error.rs L1–10;core/src/tools/context.rs L340–353。
sh 围栏里的竖线有没有误触发?quizFiles)一律不进入口播稿,仅作为集页下方的文字自测卡渲染。AGENTS.md 第 35 行和第 265 行都是失效路径。若只能先做一道自动检查,你检查带 codex-rs/ 前缀的路径,还是检查所有反引号里含 / 的字符串?写出过滤规则。没换行的 token 不可见。能确定不再变的行进终端 scrollback,提交即固化;还可能变的行,尤其是没闭合的表,只活在活动尾巴里,等流结束再一次定稿。
能局部检查的决策,不要只写在 Markdown。条款告诉人审什么,红灯在人没看的时候仍然亮。路径存在性和穷尽表,比养一台 rustc 插件更便宜,也更先该做。
可回放问出事之后能不能复原,可拒绝问出事之前有没有门。先问三个问题再决定抄哪一侧:跑在谁的机器上,处理谁的数据,失败的代价是密钥泄漏、仓库被改,还是评测不可复现。
*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*