Files
codex-app-server/docs/platform-browser.md
host 125b436ab3 browser: click-path allowlist, safe tenant script upgrade, allowlist activation plan
- click/click_ref: pre-click allowlist gate on resolvable anchor href
  (same check as navigate; miss refuses before the click executes);
  click/click_ref/click_at: post-click landing URL check.
  click_at has no pre-click gate by design (coordinates name no
  element); JS-driven navigation, evaluate, meta refresh and popups
  are documented as unintercepable with the Tinyproxy 403 backstop.
- Tenant script upgrade is now safe: only proven-unmodified scripts
  (manifest match, 2a569ac known-shipped SHA-256, missing, or
  current) auto-merge; tenant-modified scripts are never overwritten,
  are reported as skipped, and get one deduped audit record. The
  manifest never learns a tenant hash (else the next upgrade would
  mistake a modified file for untouched). Idempotent reruns.
- Activation candidate = deployed Tinyproxy programming set encoded
  with leading-dot entries (plain entries would regress
  docs.python.org); npmjs.org per the deployed filter, not npmjs.com.
  Host activation itself is NOT performed this round: only the plan,
  validation tests and rollback are delivered.
Tests: 28 new (click 13, upgrade 8, activation 7), all fake/no-network;
98 browser-related tests pass including prior suites.

(cherry picked from commit e4a93f5748ec815f163d0bc096ff4dc4b8c651f5)
2026-11-01 09:06:28 +00:00

202 lines
14 KiB
Markdown
Raw Permalink 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 browser 的对照
本轨目标:把编程任务平台的浏览器能力复刻到与 Muse browser
可对照的程度——只服务编程任务(验证自己的 Web 应用、读官方
文档、查包与 CVE),不复刻个人 agent 的通用上网能力。
范围边界先说清:
- 搜索(browser.search / browser.open / browser.find 那一路)
由另一轨负责,本轨只消费、不重建。
- 多标签页已由本轨另一路完成:`browser_new_page` / `browser_list_pages` / `browser_switch_page` / `browser_close_page`,导航白名单见下文补充。
- 本轨不部署:filter 改动只落在仓库文件里,主机 tinyproxy
未 reload;真实站点未 live 验证(见文末“未验证”一节)。
## 逐项对照
| 能力 | Muse browser | 本轨状态 |
| --- | --- | --- |
| Live Chromium(真浏览器,非文本抓取) | 有 | 已有:`cloud/playwright_browser.py`,单页 headless Chromium,1280x800 视口 |
| 导航 | 有 | 已有:`browser_navigate`;代理显式传 launch,域名不在白名单时报代理 403,不编造页面 |
| 快照(无障碍式清单) | 有 | 已有:`browser_snapshot`,及 `browser_snapshot_refs`(ref -> 元素清单,`browser_click_ref` 按 ref 点击) |
| 点击 / 填写 | 有 | 已有:`browser_click`、`browser_click_at`(坐标,有视口边界检查)、`browser_fill`、`browser_type_text` |
| 键盘 / 鼠标 | 有 | 已有:`browser_key_press`、`browser_mouse_move` |
| 截图 / 实时画面 | 有 | 已有:`browser_screenshot`、`browser_live_shot`(固定 live 文件 + JSON sidecar,供 console 轮询);截图只写任务工作区(`cwd`,文件名 basename 校验),不写共享目录 |
| JS 求值 | 有 | 已有:`browser_evaluate` |
| MCP 工具面 | 有 | 已有:`cloud/playwright_mcp.py`,stdio MCP,14 个工具(navigate / snapshot / snapshot_refs / click / click_at / click_ref / fill / type_text / key_press / mouse_move / evaluate / screenshot / live_shot / close) |
| 观看 + 接管 | 有(观看与接管) | 已有:`BrowserPanel.tsx` 是观看 + 接管面板,不是纯观看面板;`holder` 驱动——用户接管(holder=user)时 agent 工具报错、把控制权交还用户,交还后 agent 续跑前先快照确认用户改动 |
| Worker 播种 | — | 已有:worker `--playwright-mcp` 把 MCP 文件与配置播种进租户 codex-home |
| 租户工作区隔离 | — | 已有:浏览器跑在任务沙箱内,截图与下载落任务工作区 |
| 代理 403 如实报错 | — | 已有:域名不在 Tinyproxy 白名单时工具 plain 报错 403,不静默失败、不编造内容 |
| 持久 profile / 跨回合登录态 | 有 | 缺口(本轨未做):每次任务的浏览器是 fresh 会话,登录态不跨回合保留 |
| 登录凭证保险库 | 有 | 缺口 + 本轨不做:不存任何已存凭证;见“本轨不做” |
| 服务端审批卡片(表单提交 / 下载 / 上传的 Allow / Ask / Deny) | 有 | 缺口(本轨未做):本轨只有 holder 接管阻塞(用户接管时 agent 工具报错停下),没有提交/下载/上传的审批流 |
| 验证码 / 机器人检查 | 有处理流程 | 缺口(本轨未做绕过):被挡就报 blocked 停下,不绕过、不伪装 |
| 下载落盘清单 | 有 | 缺口(本轨未做):除截图落任务工作区外,无统一的下载物清单面 |
| 多标签页 | 有 | 已有(本轨补):`browser_new_page` / `browser_list_pages` / `browser_switch_page` / `browser_close_page`;新页自带全新 Playwright context,cookie/存储隔离 |
| 真实站点 live 验证 | 有 | 未做:本轨只改 egress 白名单与文档,未在主机 tinyproxy 上 reload、未对真实站点跑 live 验证 |
## 本轨不做(明确排除)
- 个人购物下单。
- 个人邮箱 / 社交登录。
- 验证码绕过、机器人检查绕过。
- 已存凭证保险库(任何真实密码、cookie、token 绝不写进
代码、测试、文档、回执)。
## 本轨改动:egress 白名单
文件:`deploy/egress/tinyproxy-filter`(Tinyproxy POSIX BASIC
正则写法,一行一条,`^\(.*\.\)*域名$` 形式;不要用 extended
锚定写法,2026-10-26 已实测 extended 写法会把允许域名也挡成
403;文件保持 644 可读)。
本轨新增 10 条(编程来源限定):
- `^\(.*\.\)*python\.org$`(含 docs.python.org 子域名规则覆盖)
- `^\(.*\.\)*nodejs\.org$`
- `^\(.*\.\)*go\.dev$`
- `^\(.*\.\)*pkg\.go\.dev$`
- `^\(.*\.\)*rust-lang\.org$`(含 docs.rust-lang.org)
- `^\(.*\.\)*developer\.mozilla\.org$`(只放行该 host,不放行
mozilla.org 通配)
- `^\(.*\.\)*crates\.io$`
- `^\(.*\.\)*stackoverflow\.com$`
- `^\(.*\.\)*cve\.mitre\.org$`
- `^\(.*\.\)*nvd\.nist\.gov$`
已在白名单、无需重复加:`github.com`、`pypi.org`、`npmjs.org`
(另有 `pythonhosted.org` 供 pip)。
刻意不动:`^searx\.diante\.me$` 精确行不动;未加任何
`diante.me` 通配(那会放行 control plane api.diante.me);
未加购物、社交、邮箱、通用新闻、视频站。
对应测试:`cloud/tests/test_websearch_worker.py` 的
`EgressFilterTestCase` 新增 2 项(见“验证”一节)——断言新规则
存在、已存在规则不重复、searx 精确行仍在且无 diante.me 通配、
以及用 BASIC 转 extended 后的实际匹配语义(子域名放行、
形似域名与后缀 trick 不放行、control plane 与购物/视频/邮箱
域名不放行)。
## 本轨补充:多标签页、导航白名单、关闭清理
- 多标签页:`browser_new_page`(开新页并切为当前页)、`browser_list_pages`(未启动时不启动、返回空列表)、`browser_switch_page`(按 index 切页,不存在时报明确错误、当前页不变、旧页 refs 清空)、`browser_close_page`(缺省关当前页,关后自动切上一页;关最后一页报错“use browser_close”)。现有单页方法全部作用于当前页、行为不变。
- 导航白名单:`BROWSER_ALLOWED_HOSTS`(逗号列表,前导点后缀匹配,大小写与端口不影响匹配)。未设置时行为不变;设置后在启动 Chromium 之前检查,未命中直接报错且不启动;`file://` 一律放行;`localhost/127.0.0.1` 无特殊待遇、须显式列出(本地开发服务器要自己加 localhost)。接线:worker 在容器启动前把 worker 环境里的 `BROWSER_ALLOWED_HOSTS`(已设置且非空时)写进租户 config 的 `[mcp_servers.playwright.env]`,MCP 进程由此拿到它;未设置时不写该键、白名单不生效(见未验证一节)。
- 页内跳转白名单(本轮补齐,上一轮的缺口已收窄、未归零):`browser_click` / `browser_click_ref` 在目标是带 href 的链接时先解析 href(相对链接按当前页展开)再做同款检查,未命中报错且点击不执行;`click` / `click_ref` / `click_at` 在点击后都会检查落点 URL,落到 blocked host 时报错。已拦住的:navigate、带 href 的 click / click_ref(事前)。确实拦不住的(经代码确认,不硬编检查):`click_at` 的事前检查(坐标对应不了元素,elementFromPoint 探针受视口/滚动影响、且漏掉 JS 驱动的跳转,故只做事后检查);无 href 目标的点击(按钮、JS 处理函数,事前无目的地);页内 JS 自行跳转(`location.href`、表单提交、SPA 路由、回车提交)、`browser_evaluate` 内的任意 JS、meta refresh、弹窗新页(`page.click` 不跟随新页)。这些路径的兜底只能是 Tinyproxy 代理 403:当白名单与代理白名单取同一域名集合时,被挡域名在代理层就加载失败;但 evaluate 与 shell 同权,白名单不是它的沙箱边界。测试:`cloud/tests/test_playwright_allowlist_click.py`(13 项,fake browser,不联网):命中/未命中/后缀 trick(`notpython.org`、`python.org.evil.com`)/端口/大小写/file/localhost 正反例、事前拒点击不执行、事后落点报错、未设置时行为不变。
- 关闭清理:`browser_close()` 关掉全部页并尽力删除 cwd 下的 `browser-live.png` 与 `browser-live.json`(删失败不报错);自定义文件名的截图文件不在清理范围内。
## 本轮补齐:老租户脚本安全升级
上一轮的播种器是“版本变化就刷新一切不同脚本”;本轮改为
安全升级(`cloud/playwright_browser.py` 的
`ensure_codex_home_playwright`,worker 播种时调用):
- 自动合并(只对已证明未改动者):播种 manifest
(租户 codex-home 里的 `playwright_scripts_manifest.json`,
播种器自己的元数据,不是租户数据)命中、或命中 2a569ac
已发布三脚本的 SHA-256(`KNOWN_SHIPPED_SCRIPT_HASHES`,
已对 origin/main blob 验过哈希)、或文件缺失(全新播种)、
或已是最新(不写)。
- 绝不覆盖:内容已改动、且两项证明都不命中的脚本,原样
不动、列入返回值的 `script_skipped_modified`(兼容旧字段
`script_differs`)、在租户 codex-home 的
`playwright_scripts_audit.jsonl` 里记一条
`skipped_modified` 审计(脚本名、租户哈希、新版哈希),
worker 同时打 warning 日志;留待人工处理。
- 幂等与防误判:重跑不重复写(已是最新不写、审计按
脚本+租户哈希去重);跳过时 manifest 绝不记录租户哈希
——否则下次升级会把改动过的脚本误判为“未改动”而覆盖
(本轮修掉的正是这一处隐患,见测试
`test_modified_rerun_does_not_rewrite_or_duplicate_audit`)。
- 升级不删除任何租户文件、不改租户已有配置语义:
`[mcp_servers.playwright]` 只在缺失时追加,现有配置段
原样保留;manifest 与审计文件只是播种器自己的附加元数据。
- 测试:`cloud/tests/test_playwright_upgrade.py`(8 项):
全新播种、未变脚本自动合并、已变脚本原样不动且有审计、
幂等重跑不重复写、无 manifest 老租户(已知哈希合并 /
未知哈希跳过)、租户数据与配置不受影响。
## 本轮补齐:BROWSER_ALLOWED_HOSTS 激活方案(本轮不执行主机激活)
候选值(域名集合 = 已部署的 Tinyproxy 编程域名集合,不编
新清单;编码用前导点形式,因为 filter 行本身就是子域名
包含式,见下文编码说明):
```
BROWSER_ALLOWED_HOSTS=.python.org,.nodejs.org,.go.dev,.pkg.go.dev,.rust-lang.org,.developer.mozilla.org,.crates.io,.stackoverflow.com,.cve.mitre.org,.nvd.nist.gov,.github.com,.pypi.org,.npmjs.org
```
编码说明(关键,不可照抄纯域名版):filter 行
`^\(.*\.\)*python\.org$` 放行 `python.org` 及其全部子域名,
而白名单的不带点条目 `python.org` 只精确匹配自身、不放行
`docs.python.org`;所以候选值必须用前导点条目,否则激活后
`docs.python.org` / `docs.rust-lang.org` 会被白名单挡掉、
相对代理白名单是回归(测试
`test_plain_entries_would_break_subdomains` 已证明这一点)。
出入说明:任务文本写的是 `npmjs.com`,但已部署 filter
(`deploy/egress/tinyproxy-filter:23`)、`deploy/new-host-checklist.md`
与本文档旧文均为 `npmjs.org`,候选值按已部署集合取
`npmjs.org`,`npmjs.com` 不在候选值内;如需放行 npm 官网
本身,那是另起的 filter 变更,不由本轮擅自决定。
测试租户/测试任务层面验证(本轮已做,全部 mock/fake,
未碰生产租户):`cloud/tests/test_playwright_activation.py`
(7 项):候选值域名集合与已部署 filter 一致;裸域名与子
域名放行、后缀 trick 与圈外域名拦截;file 放行、localhost
在候选值下被挡(见行为变更);fake 任务级 navigate 放行与
拦截(拦截时不启动 Chromium);未设置时行为不变。
对已有浏览器任务的行为变更:
- 未设置 `BROWSER_ALLOWED_HOSTS`:行为完全不变(只靠代理
403),已有任务不受影响。
- 设置候选值后:navigate 与带 href 的 click / click_ref 在
命中时不变、未命中时事前报错(navigate 不启动浏览器、
click 不执行);无 href 点击与 click_at 只在事后落点检查;
file:// 一律放行;localhost / 127.0.0.1 被挡——本地开发
服务器任务须在值里另加 `localhost`,否则会新报错,这是
已知行为变更,不是 bug。
- JS 自行跳转等拦不住的路径:行为上只剩代理 403 兜底,
与激活前一致(激活前也是代理 403)。
主机激活步骤(只交付步骤,本轮不执行):
1. 在主机 worker/service 的环境里设置上述
`BROWSER_ALLOWED_HOSTS`(与 worker 透传链路一致:worker
环境变量 → 租户 config 的 `[mcp_servers.playwright.env]`,
未设置时不写该键)。
2. 重启 worker 相关 unit(只重启、不改 unit 文件与 filter)。
3. 用一个测试任务验证:navigate 到 `docs.python.org` 成功、
navigate 到圈外域名事前报错且不启动浏览器、click 圈外
链接事前拒点、localhost 任务按预期被挡(或按需加
`localhost` 后放行)。
回滚:清空该变量(从 worker 环境里删除
`BROWSER_ALLOWED_HOSTS` 并重启相关 unit),即回到未设置
行为——已有租户 config 里的旧 env 块由下次任务准备时的
`ensure_playwright_mcp_env` 覆盖(未设置时不写该键;注意:
已写入旧值的租户 config 需一次任务准备刷新后才彻底回到
未设置行为,见 worker 透传说明)。回滚不改 filter、不删
租户数据。
是否执行主机激活:本轮不执行。执行条件是本轮测试通过
(本轮测试已通过,见验证一节)且由 parent/用户拍板——
测试不通过时只交付方案与回滚、不强行激活;本轮测试通过
不等于主机已激活,主机激活仍待明确指令。
## 安全硬线
- 任何真实密码、cookie、token 绝不写进代码、测试、文档、回执。
- 浏览器只在任务沙箱与 egress 白名单内跑;白名单外域名一律
代理 403,如实报错。
- 截图与下载落任务工作区,不写共享目录。
## 未验证(诚实边界)
- filter 未在主机 tinyproxy 上 reload(本轨不部署、不碰主机)。
- 真实站点未 live 验证:上述 10 条域名在主机生效后的实际
可达性、以及浏览器在真实站点上的端到端回合,仍待部署后
在有 egress 的环境里实测;本轨的测试只验证 filter 文件本身
的语法与匹配语义,不冒充生产可用。