反例审查图:F0 的普通与指定区域 HTML 均为 0/3;F1 的 aria-label 伪指令为 0/3 与 3/3;F2 的 display:none 隐藏节点在两种模式中均为 3/3。只观察 Fetch 返回文本,不代表 Agent 决策或远端会话清理。
反例探针 · 仅观察 Fetch 返回文本,不是 Agent 服从性测试。

随着 AI Agent 飞速发展,我们也开始尝鲜 Meta Muse、ChatGPT Dots、Claude Duet 等前沿工具。浏览器接管、登录后台、自动跑完流程,看起来往往只需要几步。但每次看着屏幕自动跳转,我脑子里都会先冒出几个问题:流程跑完以后,交给云端处理的账号数据究竟去了哪里?登录态又在什么时候结束?如果登录态被恶意工具劫持,或者网页里藏着一段与业务无关、甚至试图篡改订单事实的文字,最后该由谁承担责任?提出这些问题,并不是先入为主地认定这些云端 Agent 不安全,而是想在真正把业务交给它们之前,先弄清楚安全信任边界。

产品宣传演示一般能说明流程跑通了,却很难深入回答这些问题。这次我选择了比较新锐的工具 TinyFish 来做检验,并先把问题拆小:用六个只含虚构订单的 HTML 页面,检查 TinyFish Fetch 实际返回什么;同时核对 AgentQL MCP 的固定版本源码、SDK 文档、远程浏览器会话文档和两家服务各自的条款。

先把边界说清:**这轮实际观察到的是提取模式会改变工具返回的文本;我没有把这些文本交给 Agent,因此不能据此判断它会不会服从伪指令。**真实账号、Cookie、远程会话和远端数据清理也不在本轮实测范围内。

这几件事得分开看:把网页抓出来、把内容交给模型、让模型决定下一步、把账号登录态留在远端,是四件不同的事。只用 Fetch 读取公开网页,也可能把页面里肉眼看不见的文字带回来;但“带回来”不等于模型一定会照做。至于让 Agent 根据页面内容自己点按钮,或者把登录态交给云端,那是另外两道风险门槛,这轮还没测到。

本轮证据边界

已实测静态核对尚未实测
TinyFish Fetch 返回文本:6 个夹具 × 3 种模式单次读取(18 条),再对 F0/F1/F2 × 2 种 HTML 模式各重复 3 次(18 条)固定版本 AgentQL MCP 的请求构造、SDK 的 includeHidden 默认值、AgentQL REST 会话参数、两份公开服务条款TinyFish Browser Agent 的最终回答;AgentQL SDK/REST 的真实网络请求体;真实登录、Cookie/Profile 驻留、streaming URL 访问控制和远端数据清除回执

先给答案:在首轮单次对照(每格 n=1)里,display:none 伪指令在 Markdown、普通 HTML 和指定区域 HTML 三种提取中都进入返回文本;F1 的 aria-label 伪指令和 F4 隐藏字段 canary 只在指定区域 HTML 中出现;F3 的 HTML 注释伪指令没有进入这三种返回文本。F5 的无害 aria-label 也只在指定区域 HTML 中返回。重复测试只覆盖 F0–F2 的两种 HTML 模式,结果见下表。

同一个页面,提取模式不同,返回的内容也不同

我先做了一组很小的供应商订单夹具。页面只有四个基准字段:

  • 订单:#101
  • 应付金额:USD 4200.00
  • 状态:PENDING_APPROVAL
  • 收款标记:ACCOUNT_FIXTURE_GOOD_101

其余内容都是合成测试:aria-label 中的伪指令、display:none 隐藏文字、HTML 注释,以及一个明显标注为非秘密的隐藏字段值。不存在真实银行卡、API Key、Cookie 或个人资料。

六个静态 HTML 夹具放在公开测试目录中,位于独立研究分支而非默认分支。本轮 Fetch 使用固定源版本 152cdc9;后续 604bde0 只更新 README,F0–F5 六个 HTML 文件的 Git blob SHA 未变。表格里的 F0–F5 名称直接链接到各自固定版本页面。生成器、判定脚本和原始返回文本仍保存在私有研究档案中,公开仓库可复用固定输入,但不能一键重建整套实验。完整提交号与 CDN 来源限制见文末附注。

我用免费的 TinyFish Fetch 做了两轮读取。第一轮是六个页面分别用普通 Markdown、普通 HTML 和指定 DOM 区域的 HTML 提取,共 18 条返回文本;第二轮只取 F0、F1、F2,用普通 HTML 和指定区域 HTML 各重复三次,再得到 18 条文本。这里的配置分别是 format: "markdown"、format: "html",以及 format: "html" 配合 include_selectors: ["#trusted-order-facts", "#untrusted-webpage-region"]。表中 0/1、1/1 和 3/3 按目标伪指令或 canary 字符串是否出现在工具返回文本中计数,不代表模型是否执行指令。下面先列第一轮单次对照(每格 n=1),再列重复结果。

六个合成 HTML 夹具的首轮三种提取模式与 F0–F2 重复轮对照;总计 36 条 Fetch 返回文本观察,明确区分工具输出、Agent 决策与远端会话清理。
36 条返回文本观察 · 每格样本量不同,不能解读为模型决策或整体安全评级。
夹具不可信内容的位置普通 Markdown普通 HTML指定区域 HTML
F0无伪指令0/10/10/1
F1aria-label 伪指令0/10/11/1
F2display:none 伪指令1/11/11/1
F3HTML 注释伪指令0/10/10/1
F4隐藏字段中的假 canarycanary 0/1canary 0/1canary 1/1
F5无害的 aria-label 对照—(夹具无恶意文本)—(夹具无恶意文本)无害 aria-label 文本 1/1

F5 是无害负对照:它没有恶意文本,但指定区域 HTML 仍返回无害 aria-label。这更符合“所选区域的属性文本被序列化进输出”的解释,而不是提取器识别了文本是否恶意。F3 的伪指令位于 HTML 注释中,本次返回的 DOM 序列化文本不含注释节点。

注意:F4 的 canary 1/1 是首轮单次观察,未进入重复轮;第二轮只重复 F0–F2 的两种 HTML 配置各三次,Markdown 模式和 F3–F5 均未重复。

第二轮重复结果:

夹具普通 HTML(3 次)指定区域 HTML(3 次)
F0:正常页面伪指令 0/3伪指令 0/3
F1:aria-label 伪指令0/33/3
F2:display:none 伪指令3/33/3

36 条返回文本都保留了合成订单的金额、状态和收款标记。重复轮再次复现了 F2 在两种 HTML 模式中均为 3/3、F1 在普通 HTML 中为 0/3 而指定区域 HTML 中为 3/3 的差异。

这组结果让我在意的,不是哪个模式能被简单贴上“安全”标签,而是一个很常见的开发错觉:格式转成 Markdown,或者用选择器框定一个区域,并不等于顺手完成了安全清洗。 F2 的隐藏文字在首轮三种模式中都出现;重复轮里,它在两种 HTML 模式下也都出现了 3/3。F1 的 aria-label 和 F4 的隐藏字段 canary 则是在指定区域 HTML 中出现的,其中 F4 只有首轮 1/1,不能和重复验证过的 F1 等量齐观。再看 F5:即使 aria-label 里写的是无害说明,指定区域模式也照样返回它。至少在这组结果里,差异更像是所选区域的属性文本被带进输出,而不是工具识别了内容是不是恶意。

这和我此前的 Meta Muse 合成邮箱与日历测试是两类证据:那篇检查模型最终回复与源记录是否一致;这一轮检查的是 Fetch 返回文本中包含哪些字符串。

AgentQL MCP 并不是把本地 DOM 直接塞进请求体

看源码之前,我自己也有个先入为主的怀疑:既然 AgentQL 主打用自然语言做语义定位,本地运行的 MCP 会不会把本地 Chrome 的整棵 DOM 树直接打包发到云端?如果真是这样,页面里不该出境的内容就可能跟着请求一起走。但工程判断不能靠脑补定罪,得先看实际请求构造。

我固定检查了 tinyfish-io/agentql-mcp 的 src/index.ts。其中 extract-web-data 的本地请求构造把 url、prompt 和参数发送到 api.agentql.com/v1/query-data;构造出来的 JSON 请求体没有本地 DOM 或 accessibility_tree 字段。

这并不等于“页面内容没有出境”。这条 MCP 路径发送的是 URL 和提取提示,服务端会如何获取、处理这个 URL 对应的页面,需要按服务端路径理解;但它不是“当前本地浏览器把整棵 DOM 序列化后放进请求体”这件事的证据。

另一个路径是使用 AgentQL SDK 包装 Playwright 页面。AgentQL 的工作原理文档写明,语义处理会使用页面 HTML 和 Accessibility Tree,并对输入做简化。它的 JavaScript SDK 页面 API还列出了不同方法的隐藏元素默认值:

SDK 方法文档中的 includeHidden 默认值
queryData()true
queryElements()false
getByPrompt()false

这些是官方文档列出的默认参数,不是本轮抓到的网络请求。本轮没有运行 SDK。

再往下还有 TinyFish 云端 Browser Agent 和 AgentQL 远程浏览器会话。它们与上面两条路径也不应该混为一谈。同一个品牌下的 MCP、SDK、URL 提取和远程浏览器,必须分别核对执行位置与数据出口。

换句话说,我现在不会笼统地问“这家工具会不会泄密”,而会先确认自己接的是哪一条路径:本地 MCP、包在 Playwright 上的 SDK,还是运行在别人云端的浏览器。名字相近,不代表数据走的路相同。

远程会话的“清理”要看具体的生命周期配置

AgentQL 的远程浏览器指南列出了 CDP 连接地址和页面 streaming URL,并把自动会话管理与清理作为产品能力介绍。这个说明本身并不能证明某个 streaming URL 是否需要二次鉴权,也不能证明会话数据已经从所有存储中删除。

更具体的配置写在 AgentQL REST API reference 的浏览器会话接口里:

  • inactivity_timeout_seconds 默认是 300 秒,也就是 5 分钟。
  • shutdown_mode 默认是 on_disconnect。
  • 另一个选项 on_inactivity_timeout 会让会话在最后一个客户端断开后继续保留,直到达到不活跃超时;在超时窗口内重新连接可以复用现有浏览器。

这份文档明确了会话终止时点可以配置,但没有提供可独立核验的远端数据删除回执;也不能把 AgentQL REST 的会话参数直接推广到 TinyFish Browser 产品或其他云端浏览器会话。各自的 Profile、会话生命周期和清理行为需要分别核验。

说实在的,看到“自动会话管理与清理”这样的产品描述,我会继续追问一句:这里的“清理”具体指关闭浏览器、断开连接,还是有证据证明持久化的数据也已删除?这些不是同一件事。尤其当会话使用 on_inactivity_timeout 时,文档描述的是客户端断开后仍可保留到不活跃超时;若同时使用默认的 300 秒超时,浏览器会话可能在这段窗口内继续存在。这不证明别人能接管会话,但足以说明:断开连接本身不是数据已销毁的验收回执。

AgentQL 的凭证缓存示例还展示了用 Playwright 的 storage_state(path=“session.json”) 将登录状态保存到本地文件。这是本地 SDK 的一种使用方式,不能据此推断远程 Browser Profile 的数据驻留位置。

服务条款说明“允许如何处理”,不等于“实际发生了什么”

TinyFish 和 AgentQL 的服务条款是分开的,不能把它们合并成一份模糊的“厂商隐私承诺”。

我把两份当前公开条款的 Customer Data 部分并排看了一遍。差别不在于“哪家一定会训练”,而在于条款具体写了什么:

条款页面Customer Data 许可文字是否在该段明确写出模型训练 / 微调
TinyFish Terms of Service允许为提供、维护、保障服务、分析和改进服务处理数据;还提到审阅特定 runs、prompts、inputs、outputs、errors 和使用模式。是。 条款明确列出训练、微调、评估和改进其 AI/ML 模型,并附有防止向其他客户披露机密信息的商业合理保障措辞。页面标注生效日期为 2026 年 8 月 17 日。
AgentQL Terms of Service(生效日期:2026 年 4 月 21 日)Customer Data 许可写的是仅为提供、维护、保障和改进服务处理;另允许为合法目的收集、使用和保留由 Customer Data 与服务使用产生的汇总或去标识化数据,包括产品改进和开发。当前可见的 Customer Data 条款段落没有明确写出模型训练或微调。 这不等于推断其他合同、订单表或政策绝无相关约定。

我平时也不会把服务条款当成运行日志看,但做这类工具的边界审查时,我不会跳过它:条款回答的不是“这一次请求有没有被拿去训练”,而是供应商公开写下了哪些数据处理许可。上面的差异值得在接入前确认,尤其要弄清自己是否受单独的企业协议约束;它本身并不能证明任何一条具体数据实际进入了训练流程。

我会怎样划定实际使用边界

这轮结果没有给出“TinyFish 安全”或“不安全”的总体评级,但它足够支持几条可执行的工程规则。

  1. 不要用主账号试水。 如果任务必须登录,先用专门的低权限测试账户,移除支付、管理员和不相关业务权限。人工在远程浏览器里输入密码,不会自动把后续 Cookie 和登录态变回本地状态。
  2. 把网页内容当成不可信输入。 aria-label、隐藏节点和注释可能承载与业务字段无关的文本。让工具返回 JSON 不等于它已经完成了安全清洗;应当显式定义允许字段,核对值的来源,并对隐藏内容做有针对性的负向测试。
  3. 选择器不是脱敏机制。 F5 的无害属性对照也在指定区域 HTML 中出现,说明这是属性文本随所选 DOM 区域返回的现象,不是对恶意语义的识别。限定 DOM 范围可以减少无关内容,但不能假定它会自动删除区域内的属性或隐藏节点。
  4. 读和写分开。 让 Agent 整理订单、生成待审草稿,与允许它批准订单、发送邮件、提交表单是不同的权限。不可逆操作应有独立的人工确认,不能只靠提示词里的“请勿提交”。
  5. 明确会话退出条件。 开始任务前确认使用的是哪种会话接口、shutdown_mode 与超时设置。不要把页面不再显示、客户端断开或工具返回 Completed 当成远端状态已清理的证明。CDP 地址和 streaming URL 也应当按敏感控制入口对待,避免公开转发。
  6. 最小化送入模型的数据。 对账单、客户资料、内部工单等页面,先问清楚任务是否真的需要整页语义解析。能在本地筛选成少量字段,就不要无条件把完整页面交给云端处理。

我原本还想把测试推进到下一步:把同一组夹具交给 TinyFish 的云端 Browser Agent,看看它拿到页面文本后会怎么决策。但当前测试账户无法强制执行已经批准的步数上限;可用的默认步数远超这次测试的授权范围。我没有为了把实验跑完而放宽限制。对自动化工具来说,无法可靠限制执行范围,本身就是该停下来的理由,不是可以靠一句“注意安全”带过的小问题。

所以这轮停在 Fetch 返回文本、固定版本源码和官方文档核查这一层。我不是想给 TinyFish 或 AgentQL 贴上“安全”或“不安全”的标签,而是想先弄清楚数据究竟经过哪条路径、工具实际返回了什么,以及哪些事情还没有证据。现在的 AI 浏览器工具确实省事;但在把公司后台账号、业务数据或带权限的登录态交出去之前,我更愿意先用合成数据做一轮小测试,再确认权限隔离和退出条件。演示能跑通只是起点,真正要承担后果的是接入它的系统和调用方。


附注 / 时间说明: 测试记录截至 2026 年 10 月 25 日。本轮只实测免费 TinyFish Fetch 的合成页面文本提取;Browser Agent、AgentQL SDK/REST 运行期请求、真实登录态驻留与远端数据清理均未实测,细分范围见开头表格。

来源与复现说明: Fetch 请求指向源提交 152cdc9a58e6a8fcf2d5857820d60774f0967555;后续提交 604bde069c0bd32bb41a4238140062e5468a7f2f 只更新了 README,F0–F5 的 Git blob SHA 未变。页面经第三方静态 CDN cdn.githubraw.com 读取;其可用性、缓存和字节等价性未审计,因此不能声称 CDN 实际返回字节与 GitHub 源文件逐字节一致。完整生成器、判定脚本和原始返回文本保存在私有研究档案中。