什么是 Agent Surface
Agent Surface 是 Web 页面向外部 Agent 提供能力的一组协议边界。页面把已有业务动作投影为可发现的声明,并在受限会话中接受调用。Surface 不运行 Agent,不解析 DOM,也不规定传输;它只规定页面公开什么、外部客户端能观察到什么,以及一次调用必须经过哪些检查。
这层边界处理的是页面内外的差异。页面内代码可以直接访问运行时、状态和闭包;浏览器扩展、桌面客户端或远程 Agent 不能。外部客户端若只能依赖 DOM、截图或另一套后端 API,通常可以完成一次点击,却难以判断这个按钮代表读取还是不可逆写入、作用于哪条记录、是否需要确认,以及业务状态是否真的改变。
问题的形态
以一个订单管理页面为例。列表每行显示订单号、客户和备注,并提供行内编辑。订单号 SO-1002 是业务系统里的稳定标识;当前 DOM 中,这一行的备注输入框可能渲染为 #note-42,旁边是保存按钮。页面内部对应的动作可以声明为 update_note@order,输入是记录 id 和备注文本。
假设运营人员要把 SO-1002 的备注改为「客户要求周五前回电」。在页面内部,这段逻辑是一个已有函数;在页面外部,它只能表现为找到输入框、填入文本并触发保存这一组事件。
两者并不等价。#note-42 属于当前渲染,重新排序、翻页或组件重渲染都可能改变它;update_note@order 描述业务动作,只要记录仍在会话可见范围内,客户端就可以用 record: "SO-1002" 指定目标。Agent Surface 的交换单位是后一种,也就是页面声明过、能够被再次执行的动作。
因此,Surface 不是一套通用浏览器 API。它不提供 click(selector)、eval(script) 或任意 DOM 步骤,也不替页面推断未声明的操作。它只开放页面所有者愿意公开的能力,并把调用送回原有执行路径。
当页面不能或不愿声明能力时,外部客户端仍可使用 CDP、Playwright 或视觉自动化。那些路径解决的是如何操作一个界面;Surface 解决的是页面愿意以什么业务语义接受操作,以及这些操作受什么边界约束。两者可以并存,但不应混为一谈。
契约的三个部分
Surface 的输出和检查围绕三组信息展开。它们不是三个服务或三个进程,而是同一份契约中相互约束的部分。
| 部分 | 回答的问题 | 主要内容 |
|---|---|---|
| 能力 | 页面允许执行什么 | 稳定 id、说明、输入结构、影响类型、确认策略、作用域 |
| 上下文 | 当前页面是什么状态 | 路由、区域、记录、可引用 id、状态视图、反馈、不可用项 |
| 控制 | 谁在执行,结果是什么 | 已验证会话、作用域、有效期、撤销、确认、结构化结果、审计 |
能力是契约的核心,但单独一份动作清单不足以安全执行。客户端还需要知道动作作用于哪条记录、当前是否可用、是否需要人工确认,以及执行后业务是否改变。上下文提供选择动作所需的事实;控制限定本次会话能做什么,并为结果提供可核验的结构。
能力使用业务名称,不使用选择器。下面是一个折叠后的行内动作示例:
update_note@order
effect: write
approval: ask
inputSchema:
note: string
record:
param: recordupdate_note@order 是稳定模板。inputSchema 声明业务参数 note;record 是折叠模板的元数据,表示调用时要用名为 record 的输入指定具体行。具体记录 id 来自上下文的 recordIds,实际调用时与 note 一起放进 input。页面解析该行后执行同一套可用性、确认和权限检查。模板不会因为列表被筛选、排序或滚动而改变。
有效能力不是页面声明的全部能力。设 C_page 为页面声明的集合,C_session 为已验证会话允许的集合,则本次可用的能力为:
C_eff = C_page ∩ C_sessiondescribe 和 invoke 都只作用于 C_eff。不在交集中的能力既不会被外部客户端发现,也不能通过该会话调用。C_session 通常由宿主或传输层在验证用户、Agent、作用域和有效期后构造;Surface 本身不读取 Cookie、JWT、OAuth token 或委托密钥。
上下文同样不是页面快照。它只包含选择和检查动作所需的信息,并对省略给出说明。例如,一个有一千条订单的页面不应发送一千条完整记录,而应返回当前窗口、可引用记录 id、总数,以及按 id 读取单条记录的方式。外部客户端据此知道哪些记录可引用,但不会获得整个 DOM 或整个应用 store。
与页面内 Agent、外部 Agent 的关系
Surface 位于页面能力与外部客户端之间,不替代页面运行时:
页面业务代码
├─ 能力注册表
│ ├─ 页面内 Agent(可直接调用)
│ └─ Agent Surface ── 传输 ── 外部 Agent
└─ 页面执行器(由页面内 Agent 与 Surface 共用)页面内 Agent 是运行在页面中的客户端,可以直接读取注册表和状态,并调用执行器。外部 Agent 不共享页面闭包,因此只能通过 Surface 获得序列化的能力投影,再经传输层发起调用。两者最终使用同一份能力声明和同一个执行器。
这一关系不要求页面同时具备两类客户端。一个页面可以只有页面内 Agent,也可以只开放 Surface,或者同时提供两者。Surface 不是页面内 Agent 的远程副本;它不需要一个页面内 Agent 存在,也不规定外部客户端如何规划任务。
同样,Surface 不新增一套业务实现。它引用现有的注册表和执行器,因此动作的可用性、确认、人工接力、校验和后端请求仍由页面原有逻辑决定。若页面内 Agent 能执行某个动作而外部客户端不能,差异应来自会话作用域、确认策略或后端授权,而不是两套实现之间的行为漂移。
一次调用的路径
外部客户端通常先调用 describe,再调用 invoke。
describe 是无副作用的读取。它校验协议版本、请求 id 和会话,然后返回经过作用域裁剪的能力清单与有界上下文。下面的结构是经 handle() 或传输返回的响应包络,只保留说明选择过程所需的字段;直接调用 surface.describe() 时,返回值从 result 的内容开始,不包含外层 ok 包装。
{
"protocol": "pling.surface/0.1",
"ok": true,
"result": {
"capabilities": [
{
"id": "update_note@order",
"description": "修改订单备注",
"inputSchema": {
"type": "object",
"properties": {
"note": { "type": "string" }
},
"required": ["note"]
},
"effect": "write",
"approval": "ask",
"record": { "param": "record", "total": 48 }
}
],
"context": {
"recordIds": ["SO-1001", "SO-1002"],
"recordIdsTotal": 48,
"recordsTotal": 48
}
}
}这里的 record 是能力级元数据,不是业务输入。param: "record" 指定调用时用于选择目标记录的输入键;total: 48 表示这个模板当前覆盖 48 条可引用记录。若部分记录不在视口内,还会出现 offscreen 计数。
对应的 invoke 因而包含两个部分:inputSchema 定义的业务参数 note,以及折叠动作要求的记录 id record。Surface 用 record 还原具体动作,并在调用页面执行器前移除这个协议参数;执行器只收到业务参数。
客户端从返回结果中选择能力、记录和输入,然后发起 invoke:
{
"protocol": "pling.surface/0.1",
"id": "req_01",
"method": "invoke",
"capability": "update_note@order",
"input": { "record": "SO-1002", "note": "客户要求周五前回电" }
}页面侧处理一次 invoke 时,顺序大致如下:
- 校验请求包络、协议版本、请求 id 和方法。
- 解析已验证会话,拒绝缺失、过期或已撤销的会话。
- 计算有效能力集合,拒绝越界能力。
- 解析记录与输入,检查动作当前是否可用。
- 通过页面执行器执行;该执行器保留人工界面使用的校验、确认与业务路径。
- 返回结构化结果,并在能够观察时附带有界的
after变化。 - 按宿主配置写入元数据审计;限流和防重放等保护在调用执行器之前生效。
成功响应表示请求已被理解并按契约处理,不等同于业务一定接受了操作。执行结果需要说明动作、目标记录、成功或失败,以及可观察到的变化。需要审批时,客户端可以知道动作处于等待状态,但不能自行批准;批准发生在页面内的可信流程或业务后端。
与相邻概念的关系
| 概念 | 负责什么 | 与 Surface 的关系 |
|---|---|---|
| 页面内 Agent | 在页面中理解意图、选择动作并调用运行时 | 可以使用同一注册表和执行器;不依赖 Surface |
| 外部 Agent | 在页面外规划并调用能力 | 通过某种传输使用 Surface;Codex 只是其中一个客户端例子 |
| MCP | 把工具暴露给 Agent runtime 的协议与传输方式 | 可以承载 Surface 能力,但不定义 update_note@order 的业务含义 |
| CDP / Playwright / 视觉自动化 | 操作浏览器、DOM 或界面 | 不要求页面声明能力;可作为无契约页面的执行路径 |
| 业务后端 | 判断当前用户能否对具体资源执行操作并提交事务 | Surface 为它携带请求与结果证据,但不替代它授权 |
MCP 常被误认为 Surface 的同义词。二者解决的问题不同:MCP 规定工具如何出现在 Agent 的调用列表中,Surface 规定页面提供哪些业务能力、上下文如何裁剪、会话如何约束、结果意味着什么。一个 MCP 服务器可以转发 Surface 请求;MCP 本身不会说明 approve_order 是否需要第二位审批人,也不会证明审批已经发生。
CDP 和视觉自动化也不与 Surface 争夺同一位置。它们可以在页面没有契约时工作,代价是依赖 DOM、布局或视觉推断。Surface 提高的是有页面配合时的准确性与可审计性;它不能阻止已经拥有浏览器底层权限的客户端绕过契约。
保证与限制
在契约范围内,Surface 提供以下性质:
- 客户端只能看到和调用已声明的业务能力,不能通过协议获得通用 DOM 控制。
- 上下文有选择、有上限,并说明省略,不返回整个 DOM 或整个 store。
- 调用经过页面执行器,与人工界面共享可用性、确认和业务校验。
- Cookie、JWT、OAuth token 与委托密钥不进入协议上下文或模型上下文。
- 错误和结果具有结构;未知版本、未知方法、越界能力和不可用保护均失败关闭。
这些性质不构成以下保证:
- Surface 不保证业务后端一定接受调用。会话有效并不代表用户有权限、订单状态允许或审批已经完成。
- Surface 不提供认证、授权、传输、持久化或审计存储本身;这些由宿主实现。
- Surface 不能阻止拥有 CDP 或等价权限的 Agent 在契约之外操作页面。
pling.surface/0.1目前是带参考实现的工作草案,不是既定标准。
适用场景
Surface 适合这样的页面:业务动作已经存在且语义稳定;页面所有者愿意明确开放其中一部分;外部客户端需要在不同会话中重复执行这些动作;开放范围、确认和结果需要被约束和记录。
它不适合替代所有浏览器自动化。页面无法修改、任务需要任意探索、目标只是抓取页面或执行未声明操作时,CDP、Playwright 或视觉自动化仍然是更直接的工具。高风险操作也不应仅因为“可以声明”就开放;它们需要后端的最终授权和可信的人工确认。
当前状态
@plingjs/surface 已实现传输无关的 describe / invoke 内核,以及会话作用域、过期与撤销检查、可注入的限流与防重放保护、有界上下文、增量 after、折叠行内动作、审计元数据和审批证据剥离。参考仓库中包含页面集成与外部客户端示例。
pling.surface/0.1 是这一实现所对应的工作草案。具体页面能否安全、稳定地开放某组能力,仍需结合该页面的执行路径和后端授权进行验证。
小结
Agent Surface 让外部客户端使用页面已经声明的业务动作,而不必从 DOM 或像素重建页面。它把可调用能力、受限上下文和执行控制放在同一份契约中;动作如何实现、是否授权以及是否产生业务效果,仍由页面执行器和业务后端决定。

