Files
codex-app-server/docs/chv-muse-p2-gap.md
T
host 179a86d655 fix(chv-muse-r11fix): browser 快照改 Page.aria_snapshot()(复测新发现 2)
Playwright 1.64.1 的 Page 根本没有 accessibility 属性(那是
Puppeteer 的 API),open/snapshot 在真实库上必报
'Page' object has no attribute 'accessibility'。原生快照是
Page.aria_snapshot()(返回 ARIA 快照字符串),a11y 字段仍叫
a11y,值改为字符串;截断改字符串版 truncate_aria(200 节点
行/64KB)。FakePage 同步改真形状(旧 fake 自带 .accessibility
与真实库分叉,是与 sync_playwright bug 同款模式)。
wheel 1.64.1 实测:sync_api/_generated.py 无 def accessibility,
Page 与 Locator 均有 aria_snapshot() -> str。
2026-11-07 07:02:37 +00:00

128 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 对齐 Muse P2 差距表:客机 nspawn 单元 + 浏览器子代理独立
依据:`~/workspace/research_notes/meta-muse-ai-architecture-20261101-0755/report.md`
(官方以 Meta security.muse.ai 2026-09-08 为准;第三方拆解只作参考,
逆向细节不得写成官方架构)。Muse 模型一句话:VM 内 systemd-nspawn
运行时单元(Hatch)跑代理,单元 root 映射为 host 侧非特权用户,自有
虚拟网络接口,过滤 syscall(如禁用 io_uring),受限能力(如无
CAP_SYS_PTRACE、无 CAP_NET_ADMIN);浏览器任务由专门浏览器子代理
完成,只见页面无障碍树快照、不见原始 DOM,不能在页面上下文跑 JS,
CDP 由单元外 broker 管理。
本轨范围只限 P2:客机镜像层——systemd-nspawn 运行时单元 + 浏览器
子代理独立。不碰 P1(执行器 WarmPool/气球/vsock,常驻 VM)、不碰 P3
(authd 替身凭证、Sentinel 式出口审批)。分支 `feat-chv-muse-p2`
(从 `feat-chv-cover100` 分出,基线 HEAD `06c3e17`)。
## 1. 现状 vs Muse
| 项 | 现状(实测/代码为准) | Muse 目标 | 本轨做法 |
|---|---|---|---|
| 代理运行位置 | codex 直接跑在客机用户态(执行器经 SSH 以 `chv` 身份调 `python3 /app/host.py`,它再起 `codex app-server`);无内层隔离 | 代理跑在 systemd-nspawn 运行时单元内 | `chv-cell run -- python3 /app/host.py --state … --codex-home … <cmd>`:单命令单元(`--as-pid2`,不 boot 第二个 systemd);单元即沙箱 |
| 单元 root 映射 | 无此概念(`chv` 就是普通用户) | 单元 root 映射为 host 侧非特权用户 | `--private-users=1000:65536`:单元内 uid 0 → 客机侧 `chv`(uid 1000)。效果:workspace/`~/.codex`(`chv` 所有)单元内仍可写;host-root 0600 文件(如 `/etc/chv/guest-env`)在单元内显示为 overflow uid、不可读;再加 `--inaccessible=/etc/chv` 纵深防御 |
| 单元根文件系统 | 无(直接用客机根文件系统) | 单元自有根文件系统(完整 Debian 镜像),与 host 系统分离 | **本轮不做独立根文件系统**:单元复用客机根文件系统(`--directory=/`)。独立 minbase 根文件系统约 200–300MB,另要把 codex/playwright 依赖再装一份,会顶破 qcow2 ≤1GB 验收。文件隔离本轮是“读多写少 + 遮蔽”:`/usr`、`/app` 等 host-root 所有路径单元内只读不可写,`/etc/chv`、playwright 包与浏览器二进制 `--inaccessible` 遮掉,可写路径只有显式 bind(workspace、`~/.codex`、`/run/chv`)与 tmpfs(`/tmp`)。完整独立根文件系统列为下一阶段,需先重定镜像体积预算 |
| syscall 过滤 | 无 | 过滤 syscall,如禁用 io_uring | `--system-call-filter=@system-service ~io_uring_setup ~io_uring_enter ~io_uring_register` |
| 能力 | 客机 `chv` 是普通用户,但 codex 的 bwrap 沙箱在单元内另有问题(见下) | 无 CAP_SYS_PTRACE、无 CAP_NET_ADMIN | `--drop-capability=CAP_SYS_ADMIN,CAP_SYS_PTRACE,CAP_NET_ADMIN,CAP_NET_RAW,CAP_SYS_MODULE,CAP_SYS_RAWIO,CAP_SYS_BOOT,CAP_MKNOD` + `--no-new-privileges` |
| 单元内 codex 沙箱 | `host.py` 默认 `sandbox="workspace-write"`(bwrap) | 单元本身就是边界 | 单元内 codex 用 `danger-full-access`:bwrap 的 `--proc /proc` 挂载在嵌套命名空间里是已知走不通的形状(见 AGENTS.md 内层 proc 定位),单元内再套 bwrap 只会假隔离。执行器侧改 `--sandbox` 透传是 P1 的文件,本轨只定义接口(见 §4),不改 `executor.py` |
| 单元网络 | 无(与客机共享网络命名空间) | 单元自有虚拟网络接口 | `--network-veth`:宿主侧 `ve-chv-cell`(10.200.0.1/24)、单元侧 `host0`(10.200.0.2/24,默认路由经 10.200.0.1),客机侧 NAT 出站(MASQUERADE 走客机默认路由的上行口,动态发现,不写死口名)。单元内配网经 nsenter 进单元 init 的 netns 做(`chv-cell.py` 内实现)。veth 建不起来即大声失败,不静默回落共享网络;调试逃生口 `CHV_CELL_NETWORK=shared` 须显式设置、打日志,绝不默认 |
| 单元内外 IPC | 无(SSH 直达,host key 钉死) | Unix socket + SO_PEERCRED + 对端 ACL 的最小权限 IPC | `chv-broker`(客机侧、单元外):Unix socket `/run/chv/broker.sock`(bind 进单元)。SO_PEERCRED 校验:对端 uid 须在单元映射范围 1000–66535,且对端 pid 的 pid ns 须与 `/run/chv/cell.pid` 记的单元 init 同 ns(cell.pid 不存在时只做 uid 检查,并打日志)。操作白名单:`ping`、`sync pull|push|status`(仓目录只允许 `~/workspace` 下,`chv-sync` 以 `chv` 身份跑,不进 shell)、`browser …`(转发给浏览器子代理)。其余一律拒绝 |
| 浏览器 | Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS | 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 | `chv-browser`(客机侧、单元外独立进程,用户 `chv-browser`):Unix socket `/run/chv-browser/browser.sock`,操作只有 `open|snapshot|screenshot|close`。`open`/`snapshot` 返回标题、URL 与 Playwright `Page.aria_snapshot()` 的 ARIA 快照字符串(截断到 200 节点行/64KB);`screenshot` 落盘到 `/run/chv/shots/`(bind 进单元,单元只读路径取图)。**不暴露** `evaluate`、`content`(原始 DOM)、DevTools。单元内 playwright 包与浏览器二进制用 `--inaccessible` 遮掉,单元内跑不起自己的浏览器,只能走 broker |
| 凭证 | `/etc/chv/guest-env`(0600 root)+ `~/.git-credentials`(0600 chv),codex 在客机内直接可读后者 | 凭证由 authd 保管,代理只见替身令牌 | 本轨只做“单元看不见”:`/etc/chv` 与 `~/.git-credentials` 都 `--inaccessible`,同步走 broker 的 `sync` 操作(broker 在单元外跑 `chv-sync`,token 不进单元)。替身化本身是 P3,不在本轨冒充已做 |
| 客机启动同步 | `chv-sync boot` 由 cloud-init runcmd 以 root 跑,clone 到 `~/workspace` | — | 不动:boot clone 仍在单元外做;单元内只经 broker 做 pull/push/status |
## 2. 改了什么(本轨交付物)
- `chv-guest/chv-cell.py`(新,标准库):单元启动器。`build_nspawn_argv()` 纯函数产
nspawn 参数表(单元映射、能力丢弃、syscall 过滤、遮蔽、bind、tmpfs、veth、
`--as-pid2`、`CHV_CELL=1`);`configure_cell_network()` 产配网命令表(宿主侧
ve 接口 + NAT + nsenter 进单元配 host0);`chv-cell run -- <cmd>` 起单元、
写 `/run/chv/cell.pid`、等退出码、清理 NAT 规则;`chv-cell argv` 只打印
参数表、不启动(干检查用)。
- `chv-guest/chv-broker.py`(新,标准库):单元外 IPC broker。Unix socket
`/run/chv/broker.sock`,SO_PEERCRED + pid ns 双重校验,白名单操作见 §1,
`sync` 调 `chv-sync`(`runuser -u chv`,list argv、不进 shell),`browser`
转发 `chv-browser.sock`。
- `chv-guest/chv-browser.py`(新,标准库 + playwright 懒加载):浏览器子代理。
Unix socket `/run/chv/browser.sock`,操作 `open|snapshot|screenshot|close`,
只返回无障碍树快照(截断)+ 标题/URL/截图路径;无 `evaluate`/`content`。
- `chv-guest/build.sh`:apt 加 `systemd-container`、`iproute2`、`iptables`;
拷入 `chv-cell.py`/`chv-broker.py`/`chv-browser.py`(`--copy-in` + chmod);
建系统用户 `chv-browser`;装 `chv-broker.service`、`chv-browser.service`
并 `systemctl enable`;`/etc/tmpfiles.d/chv.conf` 建 `/run/chv` 与
`/run/chv/shots`。P1 交接:`cloud/chv/guest/vsock_notify.py` 若在仓库里
存在则一并拷入(条件式,不存在时只警告、不失败——P1 文件在本分支尚无,
合并后才有)。
- `chv-guest/buildinfo.py`:`BASE_CONTENTS` 加 `systemd-container`、
`chv-cell`、`chv-broker`、`chv-browser`;`build()` 支持可选 `extra_contents`
(vsock 接入时用,不改现有调用签名)。
- `cloud/tests/test_chv_muse_p2.py`(新):单元参数表、broker 校验与白名单、
浏览器 API schema 与快照截断、build.sh 干跑含新步骤、buildinfo 新内容项。
全部用假 socket/假进程,绝不连真实网络、不起真实 VM。
- `docs/chv-production-readiness.md`:补 P2 一节(单元隔离验收与待验证项),
旧定案不删、只追加。
## 3. 诚实边界(本轮不宣称的东西)
1. **没有独立根文件系统**:单元与客机共享根文件系统(只读挂载 + 遮蔽 +
bind),不是 Muse 的独立 Debian 镜像。一个有决心的单元内进程仍能读到
客机侧 world-readable 的文件内容(如 `/app/host.py`、系统库)。真正
的文件系统分离要等下一阶段的独立根文件系统 + 体积重预算。
2. **SO_PEERCRED 的 uid 检查不区分单元 root 与客机 `chv`**:单元映射
`--private-users=1000:65536` 让单元内 uid 0 在客机侧就是 uid 1000,
与客机 `chv` 同 uid。broker 的主校验是 pid ns 对照(`cell.pid`),
uid 范围检查只是第二道;`cell.pid` 不存在时只做 uid 检查,并打日志。
这不是“单元身份”的密码学证明,是命名空间对照。
3. **a11y-tree-only 是方向,不是已封死的强制**:浏览器 socket 不暴露
DOM/JS 操作,且单元内跑不起 playwright(已遮蔽),但单元内进程仍
有 curl/Python 标准库,可以直接发 HTTP 请求取页面原文——那不是
“浏览器 DOM 访问”,但也不是无障碍树。完整封堵(出口只走浏览器
子代理)是 P3 Sentinel 的事。
4. **性能数字全是待验证目标**:单元启动耗时、veth+NAT 对模型调用延迟
的影响、快照截断后的 token 占用——本轮无真机验证(第三棒 V-1~V-9
占着 WSL 测试机),一律记入“真机待验证清单”,不在代码/文档里写成
已测得。
5. **nspawn 参数以 systemd 255(`systemd-nspawn --help` 实测)为准**:
`--private-users=UIDBASE[:NUIDS]`、`--drop-capability`、
`--system-call-filter`、`--inaccessible`、`--network-veth`、
`--as-pid2` 均在本机 help 里确认存在;但在 CHV 客机内核(云镜像
generic 内核)里 userns/veth/idmap 的实际行为,须真机验证。
6. **WarmPool/常驻路径与本轨无关**:P1 的常驻 VM 若接进来,单元在常驻
VM 内同样起(`chv-cell run` 与 VM 生命周期无关);本轨不改执行器,
不碰 P1 的文件。
## 4. 与 P1/P3 的接口(不碰对方文件,只定契约)
- **P1 → P2(执行器接单元)**:执行器调客内 host.py 的命令从
`python3 /app/host.py …` 改为
`chv-cell run -- python3 /app/host.py --state /home/chv/.codex/host-state.json --codex-home /home/chv/.codex --sandbox danger-full-access …`。
`--sandbox danger-full-access` 只在单元内生效(单元即沙箱);非单元
路径(本地/Docker/冷启动回落)保持原沙箱不动。改动在 `executor.py`,
由 P1 做,本轨只提供客机侧 `chv-cell` 命令与本节契约。
- **P1 → P2(vsock 进镜像)**:P1 的 `cloud/chv/guest/vsock_notify.py`
由本轨 `build.sh` 条件拷入(§2),合并后自动生效;P2 不改该脚本内容。
- **P2 → P3(broker 预留)**:broker 的 `sync` 操作现在单元外直接跑
`chv-sync`(token 仍在客机侧落盘);P3 做 authd 替身化时,只改 broker
内部的凭证获取方式(调 authd 拿短期替身),不改 broker 的 socket 协议
与白名单。浏览器出口审批(只允许经 `chv-browser` 出站)同样是 P3 的
Sentinel 接管点,本轨只做到“单元内无浏览器可跑”。
## 5. 真机待验证清单(round 11,测试机空闲后)
- lite 验收照旧:qcow2 ≤1GB、冷启动到 SSH、codex 版本、headless-shell
例、Gitea 双向同步(boot clone + push + pull + 分叉失败关闭)。
- 单元隔离验收(新增,否定测试为主):
1. 单元内 `cat /etc/chv/guest-env` 失败(遮蔽 + 0600 overflow uid)。
2. 单元内 `cat ~/.git-credentials` 失败;经 broker `sync status` 成功。
3. 单元内写 `/usr/local/bin/x` 失败(只读根文件系统)。
4. 单元内 `nsenter`/ptrace 客机侧进程失败(pid ns + dropped caps)。
5. 含 `io_uring_setup` 的程序在单元内报 EPERM/ENOSYS(syscall 过滤)。
6. 单元内 `python3 -c "import playwright"` 失败(已遮蔽);经 broker
`browser open https://example.com` 返回标题 Example Domain 与
无障碍树快照。
7. 单元内 codex 一轮 turn 完成(`danger-full-access` 下,不再套 bwrap)。
8. 单元内出站到模型网关通(veth + NAT);`chv-cell` 退出后 NAT 规则已清理。
9. broker 拒绝非单元进程的连接(pid ns 不符时拒绝;`cell.pid` 缺失时
只做 uid 检查并打日志——该降级须在日志里可见)。
- 性能记录(只记录、不设门):单元启动到 codex 就绪耗时、veth+NAT 下
模型调用往返、qcow2 实际占用(systemd-container 与新脚本的增量)。