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

14 KiB
Raw Permalink Blame History

平台浏览器:与 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 文件本身 的语法与匹配语义,不冒充生产可用。