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

13 KiB
Raw Blame History

对齐 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
浏览器 Playwright headless-shell 由 agent 在客机内直接跑,能读原始 DOM、能跑 JS 浏览器任务由专门浏览器子代理完成,只见无障碍树快照,不见原始 DOM,不能在页面上下文跑 JS,CDP 由单元外 broker 管理 chv-browser(客机侧、单元外独立进程,用户 chv-browser):Unix socket /run/chv-browser/browser.sock,操作只有 `open
凭证 /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 与新脚本的增量)。