跳转至

15. Web、Desktop、ACP 与远端 API:让 Agent 成为可交互产品

源码链接固定官方主分支 da00f7f5358f2949383b35c14f548bc20187d80c。SDK 实验仍使用 0.2.0-rc.2;本章是主分支控制面研究,不把源码目录中的每个插件都假定成可独立安装的 rc.2 产品。先理解三种入口的职责,再决定自己的 Agent 要暴露哪些控制。

15.1 三种入口复用底座,但不共享一个万能协议

入口 主要载体 面向谁 应该依赖什么
SDK stdio 自有 JSON-RPC 自己的程序 三请求协议、Session 事件、进程所有权
ACP stdio Agent Client Protocol 编辑器/自动化 Agent 客户端 标准会话/模型/MCP/取消/恢复/一次审批
Web / Desktop Web 文档 HTTP、远端流;Desktop 附加 IPC/自定义 URL scheme 人类操作员 BFF 控制器、投影、权限交互、资源接口

Web API 不是给 SDK 多加几个 method。ACP 同样使用 JSON-RPC,也不意味着它实现第 14 章的 SDK 线协议。一个可靠的应用适配器应明确自己实现哪份协议。ACP 注册表 SDK 请求表

flowchart TB Browser[浏览器 Web UI] --> Admission[Host/Origin 校验与浏览器会话认证] Desktop[Electron 壳] --> Carrier[自定义 scheme / IPC / Web 转发] Carrier --> Admission Admission --> Connection[Connection:RPC / Fetch / Stream 载体] Connection --> Gateway[Typert Gateway / BFF Remote Services] Gateway --> Controller[SessionController 等应用控制器] Controller --> Core[Agent / Sessions / Tools / Providers] ACP[ACP 自动化客户端] --> Bridge[ACP Bridge] SDK[TS / Python 应用] --> SDKServer[SDK JSON-RPC Server] Bridge --> Core SDKServer --> Core Core --> Log[持久化 Session 日志] Log --> Projection[纯投影 / 历史分页 / 查询] Projection --> Gateway

这张图最关键的边界是“控制器”与“载体”。HTTP、IPC、stdio 解决如何传递数据;控制器决定是否创建 Agent、恢复 Agent、允许哪些资源引用,以及返回什么状态。不要把更换 WebSocket 实现当成完成权限设计。

15.2 WebServer 只承载路由,不自己认证操作者

host-webserver 提供精确路径、前缀路径、fallback 和 upgrade 注册表。精确匹配优先,前缀选最长;同类型同路径重复注册会失败,防止两个插件悄悄抢占接口。它拥有连接和卸载清理,但浏览器授权由 client-connection 完成。gzip 配置也在载体层,SSE 和 Content-Range 等情况不会被普通压缩路径误处理。WebServer 完整路由实现

client-connection 建立共享 RPC/Fetch 注册表;WebServer 存在时才安装 /api HTTP 桥。处理顺序是:校验可信 authority,创建 BrowserAuth,接收 API 请求时先 connection.admit(),被拒绝立即 401/403,通过后进入 connection/request waterfall 和有限 JSON body 桥。Connection 安装流程

配置中 trustedHosts 是标准化的 authority(主机或主机:端口)列表。默认 loopback 被允许;开放 0.0.0.0 后,部署者要声明到达该服务器的名字。Host/Origin、Sec-Fetch-Site 检查是防重绑定/跨站请求的信任围栏,不能代替浏览器登录态,更不能代替多租户权限。API 请求信任校验

身体积也属于真实边界:共享请求有上限;带图片的 JSON 使用 base64 膨胀,Connection 启动时核对上限能否容纳配置允许的 aggregate image bytes 加 envelope 余量。容量不一致直接加载失败,避免 UI 允许上传、桥层却固定拒绝的配置。图片 body 容量检查

BrowserAuth 在 runtime 根上下文生成随机启动 token,管理持久化签名 secret 和浏览器授权 cookie。启动链接是进入操作界面的凭据:在符合条件的根页面 GET 上兑换 cookie,重定向到去掉 token 的页面;不是向任意 /api 请求附一个 query token 就能调用接口。认证类

sequenceDiagram participant Operator as 操作员 participant Root as 根页面入口 participant Auth as BrowserAuth participant API as /api Operator->>Root: 启动 URL,含 token Root->>Auth: 校验兑换条件 Auth-->>Operator: HttpOnly / SameSite=Strict cookie Root-->>Operator: 303 到清理后的页面地址 Operator->>API: Cookie + 可信 Host/Origin API->>Auth: admission alt 缺少有效授权 API-->>Operator: 401 / 403 else 有效操作员 API-->>Operator: 控制器结果或流 end

cookie 绑定 authority,有限期,签名比较采用 timing-safe 路径。默认寿命 30 天。源码当前构造的 cookie 属性不能被描述为“无条件带 Secure”;反向代理 HTTPS 的安全部署必须结合实际配置验证。启动 URL、cookie 和签名 secret 都应作为敏感数据保护,不能写进静态公开课程日志。Cookie 构造与校验 配置默认值

这个机制授权的是使用此 Harness 的操作员。它没有为 SaaS 自动提供 tenantId、用户行级过滤、每用户工作空间 ACL 或独立模型账户。把 Harness 的控制端直接开放到互联网,应理解授权浏览器拿到的是控制本 runtime 的能力。公开静态课程站无需代理真实 Harness /api。

15.4 Typert:显式 Remote 服务,而非暴露整个 Context

Typert Gateway 把明确的 Remote 元数据转为远端调用。客户端应通过 remote facade/generated contracts 使用能力;不要 import 服务器 Gateway 后假装是浏览器共享库。Gateway 查找方法 descriptor,解析服务的 receiver Context,验证参数和 lookup codec,在声明的最后信号参数位置注入取消信号,并携带 invocation 信息给调用范围。远端调用准备

阅读 prepareInvocation() 时要顺着调用栈看:

  1. namespace/method 对应一个 descriptor;不存在就是不可调用。
  2. 参数个数与声明结构校验;不同参数可以采用普通 JSON 或 lookup 编码。
  3. lookup 必须在正确上下文解析对象,不是让客户端传任意对象指针。
  4. 服务 receiver 的存在及 binding 状态再次核对,防热卸载后调用失效服务。
  5. 建立带 invocation 的 scoped view,再调用实现。

strict endpoint 曾注册后撤回,Gateway 不会偷偷退回源码推断方式继续暴露它。源码推断也只识别声明的 Remote marker,不是遍历所有 ctx.* 服务开放远端访问。这是可见性边界,仍需具体控制器保证业务权限。Descriptor 查找与退回条件

事件流另有开关与生命周期。远端 subscribe 是能力契约的一部分,不应把任意 Cordis event 当作自动广播数据。一个 changed 通知可能仅表示失效,需要再读状态,而不是完整状态快照。写自己的 Agent 插件时,先判断“这个方法应不应远程调用”,然后定义参数、接收范围、取消和输出,再写 UI。远端契约说明

15.5 SessionController:浏览、激活、提交和取消分开

冷会话是已经持久化、当前没有活动 Agent 的 Session。读取历史和投影不应启动模型。follow() 会订阅相关事件后再读取 baseline,避免在异步读取快照期间漏掉新事件。它返回 header/cursor、近期消息、投影和 asOf,再跟随连续变化。输出的 cursor 属于可验证的日志前缀,不是 UI 自己随便累加的计数器。历史跟随与快照

flowchart LR Open[打开旧会话] --> Read[读持久化日志 / 投影] Read --> UI[展示历史,无模型调用] Continue[用户提交继续任务] --> Resolve[控制器 resolve / resume] Resolve --> Verify[身份 / cwd / ownership / preset 检查] Verify --> Agent[恢复活动 Agent] Agent --> Inbox[提交新 inbox 消息] Inbox --> Turn[下一活动轮次]

恢复路径观察持久化 Session,核对 sessionId、cwd 存在及所属限制,恢复 setup/preset,再调用 ctx.agents.resume。create-or-adopt 碰到已有持久身份时核对 cwd/preset,并进入恢复,区别于 SDK 对未知 ID 的 create。Agent 恢复路径

prompt 控制器还处理命令路由、内容与附件引用的可达性。客户端不能把另一个 Session 中已知的附件 ID 随便贴到当前会话当成授权。cancel 是请求活动 Agent 取消当前活动,不等于删除 Session,不等于清空持久 inbox 中全部等待消息。Session 控制方法

对自己的产品而言,按钮文字应该与这种语义一致:“停止当前执行”“继续此会话”“删除数据”是不同操作。取消请求成功也应继续观察最终状态和工具取消结果;不可承诺已经启动的第三方副作用一定撤销。

15.6 两套时间线:持久化语义与助手实时流

UI 需要在模型生成时看见文字,而持久化 Session 需要稳定、可重放的事件。Web 控制面提供可选的 assistant stream,含 attempt/revision/index 等临时投影信息;模型尝试改变、revision 重启或片段不连续时会清理/重置活跃显示。它不是把每个原始 delta 都当成独立最终消息存入日志。助手流状态机

数据 用途 重连时如何理解
持久 Session 事件 历史、工具语义、审计、投影重放 按 cursor 读稳定前缀
当前 assistant stream 活跃生成的即时 UI 用 baseline/attempt/revision 连续性恢复当前展示
sessionControl 状态流 宿主可见会话状态总览 baseline 后 changed 替换帧

跟随历史时,先订阅再快照、缓冲事件、按 cursor 去重并要求下一 seq 连续。发现 gap 应触发恢复/错误处理,不能在 UI 中补一个想象出来的事件。控制状态流也拥有队列取消和清理;页面卸载必须关闭订阅,否则会遗留资源。历史缓冲边界 宿主控制 baseline

第 14 章的 SDK 收到的是语义 Session 通知,不能据此宣称它提供 Web 相同的 raw token stream。自己的聊天 UI 若使用 SDK,可以显示工具/状态并在消息提交时更新文本;要复用 Web 的实时生成展示,应走它的相应控制契约。

15.7 附件:先入库,再记录稳定引用

附件 admission 验证 canonical base64、媒体、数量、单图与 aggregate 大小等规则。图片批量 admission 先完成必要验证,再依次保存,返回稳定引用。图片经过检测与配置的归一化策略后,对归一化后的字节计算 SHA-256;因此 ID 不保证等于用户原始上传文件的 hash。admission 图片 prepare

sequenceDiagram participant Client as 客户端 participant Store as 附件 provider participant Disk as 内容寻址对象库 participant Session as Session Client->>Store: 上传字节 / MIME / 名称 Store->>Store: 检测、上限、图片归一化 Store->>Disk: staging 写入并 fsync Store->>Disk: 独占发布 / EEXIST 时核对 digest Disk-->>Store: 稳定对象已发布 Store-->>Client: sha256 引用 + 事实字段 Client->>Session: 消息引用附件 Note over Disk,Session: 引用进入检查点前,对象需要已经持久化

本地 provider 的对象目录按 digest 分片,临时文件独占创建、同步后以 hard link 发布;已存在对象做 digest 验证,文件模式设只读,目录同步后返回。普通文件使用原始字节内容寻址,并另外建立安全显示文件名 alias;流读取校验大小和 digest。对象发布 普通文件保存和读取

附件 hash 是内容身份,不是授权票据。上传成功、附件存在、会话可引用、客户端能下载,是相互关联但不同的判断。自己的多用户产品要在 BFF 控制器加入资源所属关系及租户过滤。

15.8 交付物 present:展示文件并不是复制成不可变资产

交付工具要求已有普通文件、执行 Agent 和开放 turn boundary,限制一次文件数量,拒绝入口明显是非普通文件的情况,经 filesystem provider resolve/stat 验证。结果先放入当前执行的 pending 信息;最终 tools/result 成功后才追加 deliverables/presented。工具失败不能留下一个“已交付”日志事件。present 工具完整实现

需要注意两个源码细节。配置默认 maxFiles 是 8,而工具描述中的建议文字写“最多 4”;实际准入应以配置及代码为准,课程不把描述文字当成硬限制。交付事件记录路径等信息,不是自动把内容复制进 immutable attachment store。后来文件被修改或移走,展示结果可能改变或不可读。要稳定交付,自己的工具应复制/固化到专用对象库,并把 hash、大小、产物版本作为业务结果验收。

/api/file 资源接口接受绝对路径、通过 filesystem provider 解析和读取,设置 private/no-store、nosniff、sandbox CSP 等响应属性,处理文件不存在、拒绝、超限和取消。它不是“只有本 Session 的附件路径才可读取”的天然限制;源码明确路径/MIME 不构成访问约束,实际访问依赖已认证操作员和 filesystem provider。媒体资源 Fetch 接口

因此,不要把允许 Web 登录误读为只能读某张附件,也不要把 present 误读为经过一次额外沙箱审批。部署自己的 Agent 时,先收窄 filesystem 能力,再决定哪些文件下载入口可见。

15.9 Desktop 是 Web 应用的壳和宿主生命周期

Electron 主进程拥有窗口、自定义 scheme、有限 IPC、更新与 Host 子进程;Host 中仍运行共同的 Web 应用。DesktopHostProcess.start() 启动捆绑 runtime 内的 desktop-host,使用 Node mode 和 IPC,等待 child 的 ready URL;invalid IPC 或 fatal 会触发失败处理。Desktop Host 进程

自定义 scheme 的 shell 文档来自 Electron app bundle;app 文档静态资源来自捆绑的 Web frontend。其他 app 请求在 Host 就绪后转发给 Web 端并附相应 cookie;Host 不可用时返回 503。boot IPC 要核对 sender,返回注入信息和 stream 基址。这说明 Desktop 搬运的是同一个 Web 控制面,不能因为页面来自本地文件就省掉 API admission。Desktop scheme 与 boot

停止 Host 先通过 IPC 请求 shutdown,等待 10 秒,再按需要 SIGTERM/5 秒、SIGKILL/5 秒。更新要求的 graceful 停止还核对 exit code 和 shutdown-complete,不能只看进程消失。Desktop 停止确认

开发自己的桌面 Agent 时,应把受信任主进程的命令能力与 renderer 的不可信输入分开。窗口 IPC 要限定 owned frame 和消息形状;对外 URL 经 shell 打开;runtime 资源版本需要配对。课程没有运行 Electron 的平台端到端测试,这里是源码调用路径结论。

15.10 ACP:自动化、恢复和一次权限决定

ACP 初始化声明协议版本、模型图片能力、MCP HTTP 能力,以及 session close/list/resume;authMethods 空,authenticate 为 no-op。这是受信任程序间 stdio 自动化接口的设计,不能当成互联网用户身份认证。ACP 初始化

session/new 创建 Bridge 所有的 Agent,解析 cwd/MCP 配置,flush 空会话使其持久化后返回。session/resume 拒绝已激活 ID、缺失会话、subagent/parent lineage 会话,验证实际 cwd 一致后恢复。目录比较使用真实路径相关检查,不能只比较输入字符串。ACP 新建与恢复

session/prompt 只接受 Bridge 自己管理的 record;record 关联 inbox messageId 与 turn,按已提交 assistant/tool 事件发送标准 update,并等待 admission 完成、Agent idle、输出 drain 后返回 stopReason。cancel 会中止 admission 或请求当前 Agent 取消。关闭会话则取消、等空闲、drain 更新及可继续子 Agent、flush、dispose;多项失败可组合报告。ACP prompt/close/cancel 路由 ACP record 的结算

权限请求只在 Bridge 拥有的 Agent 且有 callId 时接管:先 drain 更新,再请求客户端 session/request_permission;只给 allow-once / reject-once,不从未知选项推断永久授权。cancelled 保持取消,只有精确 allow-once 返回一次许可。ACP 一次权限决定

ACP 的 scope 是自动化接口。它不负责 Web 的人工问答、计划评审、完整 UI 投影。自己的应用可以在 ACP 客户端实现机器审批策略,但策略的默认值必须清楚,未知响应要保守拒绝。

15.11 怎样为自己的 Agent 选控制面

先从最小产品需求出发:后台批处理选 SDK,编辑器标准接入研究 ACP,带人工审批和历史操作的产品研究 Web BFF。复用已有底座后,新增业务 Remote service 要回答四件事:谁能调用;输入如何验证;调用是否激活/改变 Agent;返回结果和订阅何时结束。

建议从一个只读业务面板做起,读 projection 而不是启动模型;再加受控 prompt;最后加资源下载和人工交互。每加一项动作,都把取消、重连、冷会话、服务热卸载和权限失败视作正常状态。一个“看起来实时”的页面不能取代这些语义契约。