Agent Tool Connectors:统一 Agent 接入
SaaS → 平台 → 三方 Agent。任何三方 Agent 只接入平台一次(一行 MCP 配置,可选一个 PATH 目录),即可使用该用户已连接的全部 SaaS 的 MCP 工具与 Native CLI;此后平台新增任何 SaaS,Agent 端零变更。
1. 目标与核心原则
平台是用户与 SaaS 之间唯一的授权与凭据边界,是 Agent 与 SaaS 之间唯一的接入面。产品命题:把 Agent 侧接入面收敛为标准、静态、无 Secret 的最小合同。
2. 系统结构
四个部件、三层。云端 Gateway 是控制面与聚合面;用户本机的 Host Runtime(可选安装)承载凭据注入;Agent 只见到一个 URL 和一个 PATH 目录。
部件职责边界
| 部件 | 位置 | 职责 | 明确不做 |
|---|---|---|---|
| Cloud Gateway | 云端(Workers + D1) | 用户身份、Connection、SaaS OAuth、Catalog 路由、凭据加密存储、聚合 /mcp、审计与通知 | 不执行 CLI、不下发 SaaS Token |
| MCP Forwarder | 用户本机 loopback | 把 Agent 的无 Token 请求注入当前短期 Gateway Token 后转发到 /mcp;续期完全内化 | 不解析业务、不存 SaaS Token |
| CLI shims + proxy + User Vault | 用户本机 loopback | 让官方 CLI(如 ntn)免登录可用;SaaS Token 存本机 User Vault,由 proxy 注入 | 不把 CLI 包装成 MCP 工具 |
| Agent | 任意 | 调 tools/list / tools/call、shell 跑 CLI | 不管理凭据、不感知路由差异 |
Host 侧进程与权限模型
- 一个 User 绑定一台 active Linux Host;Host 无任何公网监听,控制通道由 Host 出站建立。
- 平台服务(Forwarder、Vault、proxy)以专用
agent-connectors系统 UID 运行;Agent 使用另一个无特权 UID。两者数据目录互不可读写。 - Agent UID 不能读取任何 Token 文件;它只能通过 loopback 端点使用能力。
3. 用户流程
用户永远不需要:管理 Token、改 Agent 配置、感知某家 SaaS 走哪条路径。连接对象是"用户 ↔ SaaS 账号",与 Agent 无关——连一次 Slack,用户所有 Agent 都能用;换 Agent 不需要重新授权。
对话内连接(平台 meta-tools)
Gateway 通过同一个 /mcp 暴露连接管理元工具:sider.list_connections(已连接/可连接清单)、sider.request_connection(发起授权,返回浏览器 URL)、sider.connection_status(轮询结果)。Agent 由此在对话内引导用户完成连接,无需各家自建连接 UI。
sider.connection_status → 发现 Slack 未连接;调 sider.request_connection → 平台返回授权 URLsider.connection_status → connected,继续执行任务4. 连接管理界面:Portal 与接入方自建 UI
连接管理有两个消费方:终端用户(用平台自带 Portal)与有界面能力的接入方产品(在自己的应用里为其用户内嵌连接管理)。两者共享同一套底层:Portal 本身就是 Connections API 的第一个客户,接入方能做到的 Portal 都能做到,反之亦然。
4.1 连接状态机(所有界面共享)
| 状态 | 含义 | 可用操作 |
|---|---|---|
not_connected | 可连接,未授权 | 连接 |
connecting | 已发起授权,等待用户在浏览器完成(attempt 有效期 5 分钟) | 轮询状态 / 取消 |
connected | 授权完成,工具已出现在该用户全部 Agent 的 tools/list | 管理、断开、连接另一个账号 |
reconnect_required | Token 失效且无法刷新(被撤销/过期) | 重新连接(沿用同一账号身份,不产生重复 Connection) |
cancelled / failed | 用户拒绝授权 / 流程失败(带安全错误码) | 重试连接 |
4.2 平台 Portal 原型(可交互)
下面是可交互原型(模拟数据)。点"连接"会打开模拟的第三方授权窗口——先是该 SaaS 自己的登录页,然后是授权同意页(scope 列表,可授权或拒绝),成功后经固定落地页跳回 Portal。其余可体验:Supabase 的 projectRef 额外输入、等待授权的轮询态、管理抽屉(多账号、审计摘要、通知开关、二次确认断开)、失效重连、Host Runtime 未安装时 native_cli 的安装引导(右上角开关模拟)。交互合同以 4.1 状态机与 4.3 API 为准。
工具连接
- 连接点击后跳转 SaaS 授权页;回调落地页只有三态(成功 / 已取消 / 失败),不含任何参数回显,数秒后自动回到目录页。
- 需要额外输入的 Provider(如 Supabase 的
projectRef)在跳转前以单字段表单收集,输入约束由 Catalog 声明。 native_cli类 Provider 在 Host Runtime 未安装时,连接入口显示"需要本机组件"并引导安装,而不是失败。- 多账号:同一 SaaS 显示为同一张卡的多个账号行(以
accountHint区分),不是多张卡。
4.3 接入方自建 UI:Connections API
面向接入方应用界面的 HTTP API(meta-tools 面向 Agent 的 LLM,二者能力对等、同一底层)。接入方通过平台 OAuth 为其用户取得 user-scoped 凭据(scope:connections:manage)后调用。
| 接口 | 作用 | 返回要点 |
|---|---|---|
GET /api/connections | 目录 + 状态合并视图 | 每家 SaaS:id、名称、图标、状态、accounts[](含 accountHint)、requiresHostRuntime、额外输入 schema(如 projectRef) |
POST /api/connections/{provider}/start | 发起授权 | authorizeUrl + attemptId + 过期时间 |
GET /api/connections/attempts/{attemptId} | 轮询授权结果 | pending / connected / cancelled / failed(安全错误码,无 Provider 细节回显) |
DELETE /api/connections/{provider}/accounts/{accountId} | 断开指定账号 | 幂等;成功即凭据删除、工具消失 |
GET /api/audit?connection=… | 审计摘要 | 分页的调用摘要(tool、风险、时间、结果摘要) |
PUT /api/settings/notifications | 通知开关 | delete 类事后通知的开/关 |
4.4 推荐接入界面(接入方按此蓝图实现)
GET /api/connections:每家 SaaS 一张卡(图标、名称、状态徽标、账号 hint、主按钮 连接/管理/重连)。start → 在系统浏览器/新窗口打开 authorizeUrl → 以 2 秒间隔轮询 attempt → 成功后卡片即时翻转为已连接。reconnect_required 卡片置顶提示重连。- 授权必须在系统浏览器或独立窗口完成;禁止内嵌 WebView 代填——接入方永远接触不到用户的 SaaS 凭据。
- 断开必须二次确认,并明确后果:该账号工具立即从所有 Agent 消失、凭据删除。
- 多账号 Provider 必须展示
accountHint,调用时由用户或 Agent 用accountId选择。 - 连接成功后建议就地提示"你的 Agent 现在可以使用 <SaaS> 的工具了",把连接动作与能力生效直接关联。
4.5 会话内入口示例:Sider Channel 输入框(可交互)
4.4 蓝图在 Sider Channel 里的具体落点:输入框左下角常驻"连接器"按钮(对应产品截图中标注的位置,交互对标 Claude 的 "Add connector" 菜单)。点击向上弹出连接器菜单——已连接的显示状态、未连接的可直接发起(走同一个第三方授权窗口)。下面的示例与 4.2 的 Portal 原型共享同一份状态:在菜单里连接 Notion,Portal 的卡片会实时同步,反之亦然——这正是"菜单只是 Connections API 的另一个消费者"的直观演示。
- 菜单内直接发起连接:同一个第三方授权窗口、同一个状态机;连接完成后工具经
tools/list_changed立即可用于当前会话,菜单徽标数同步 +1。 - 菜单是轻量入口,只做状态一览与发起连接;账号管理、审计、断开等重操作跳转 Portal(或接入方的完整管理页)。
- 入口按钮常驻但不打扰:无连接时引导首连,有失效连接时显示琥珀色状态提示重连。
5. Agent 接入合同
合同全部内容只有两项。没有按品牌定制的 Adapter、没有平台对 Agent 配置文件的写入与所有权、没有续期触碰。
https://<gateway>/mcp
标准 MCP OAuth(见架构决策 D2)。适用纯 MCP、hosted / 远程 Agent。
http://127.0.0.1:<port>/i/<bindingId>/mcp
无需任何凭据,Forwarder 逐请求注入短期 Token。适用需要 CLI 或本地 Agent。两种形态下 Agent 配置都是一行 URL,Agent 不需要知道自己处于哪种形态。
目标 Agent 的接入命令(2026-07-29 实测)
# Claude Code —— 写入 .mcp.json(project scope)或用户配置 claude mcp add --transport http connectors http://127.0.0.1:14323/i/<bindingId>/mcp # Codex —— 写入 ~/.codex/config.toml 的 [mcp_servers.connectors] codex mcp add connectors --url http://127.0.0.1:14323/i/<bindingId>/mcp # OpenClaw —— probe 成功后写入 openclaw.json 的 mcp.servers.connectors openclaw mcp add connectors --url http://127.0.0.1:14323/i/<bindingId>/mcp --transport streamable-http
| Agent(实测版本) | 落盘位置 | 重复 add | 删除 | 备注 |
|---|---|---|---|---|
| Claude Code 2.1.220 | .mcp.json / 用户配置 | 报错退出(接入脚本需先 remove 或检测已存在) | claude mcp remove 干净 | 支持 --header、OAuth 参数 |
| Codex 0.144.6 | config.toml | 覆盖式幂等 | codex mcp remove 干净 | 支持 bearer-token env var、mcp login |
| OpenClaw 2026.7.1 | openclaw.json | 覆盖式幂等 | mcp remove | add 自带连接 probe;mcp reload 热加载;工具名自动 provider-safe 重命名 |
{"url": "…", "transport": "streamable-http"}——正是本方案要求的形态。Agent 能力矩阵(新 Agent 按能力接入,不做品牌枚举)
| 能力 | 获得什么 | 缺失时 |
|---|---|---|
| 可配置 HTTP MCP server | 全部 gateway_mcp 工具(builtin + remote_mcp) | 无法接入(三家目标 Agent 及主流 Agent 均具备) |
| 可注入 PATH / 运行 shell | 全部 native_cli。会话型 Agent(Claude Code / Codex)继承用户 shell PATH;常驻型 Agent(OpenClaw)用官方 config set tools.exec.pathPrepend(实测热生效,无需重启) | 只失去 CLI 路径,MCP 不受影响 |
支持 tools/list_changed | 会话中实时感知新连接的 SaaS | 下次 tools/list 时感知(体验降级,功能不缺) |
| 支持标准 MCP OAuth | 云直连形态 | 使用本机 Forwarder 形态 |
6. 三条调用路径
Agent 侧完全统一(tools/list → tools/call);每家 SaaS 走哪条路径由平台 Catalog 决定,Agent、Host 与用户都不能选择或覆盖。Catalog 是 Provider trust root。
初始 Provider Catalog
| 路径 | Provider | 执行位置 |
|---|---|---|
| builtin | Linear、GitHub、Google(Drive / Docs) | Gateway 直调 SaaS API |
| remote_mcp | Slack、Box、Supabase、Atlassian | Gateway 代理官方 Hosted MCP |
| native_cli | Notion(ntn) | Agent 本机 shell 直跑官方 CLI |
Agent --tools/call linear.create_issue--> Forwarder(注入短期Token)--> Gateway /mcp
Gateway: 鉴权 -> 定位该用户的 Linear Connection -> Vault 解密 Linear Token
-> 调 Linear API -> 归一化结果(无凭据泄漏)-> 返回
Agent --tools/call slack.post_message--> Gateway /mcp
Gateway: 定位用户的 Slack Connection + Token -> 以平台身份创建独立上游 MCP session
-> 调 mcp.slack.com -> 归一化结果 -> 返回
上游 endpoint、OAuth Token、MCP session 永远只在平台侧。上游合同要点见第 9 节。
Agent shell: ntn search "会议记录"
PATH 命中 shim -> 本机凭据注入 proxy(loopback)-> 注入用户 Notion Token -> Notion API
CLI 的 argv / stdout 不经过云端;Gateway 不接收 CLI 输入输出,也不把 CLI 子命令包装为 MCP 工具。SaaS 授权走浏览器 → Gateway 固定 callback → 一次性 relay 到该用户 Host 的 User Vault,Token 只落在本机。
tools/list 返回该用户当前已连接 SaaS 的工具集合;Gateway 必须声明并实现 tools.listChanged 通知。平台上新一家 SaaS = 只改 Catalog → 用户授权后其所有 Agent 自动出现新工具,用户端与 Agent 端零变更。7. 审计与可见性
连接即全可用:用户在 OAuth 授权时已经给出同意,平台不做事前审批、不打断任何调用。交互式确认("要不要执行这一步")是 Agent 侧的既有职责——人与对话上下文都在 Agent 那一侧(如 Claude Code 的工具确认、OpenClaw 的 ask 模式)。平台提供的是被动可见性与事后控制,对三条路径一致生效。
三层可见性(全部零打断)
| 能力 | 行为 | 默认 |
|---|---|---|
| 审计 | 每次工具调用记录 User、Provider、canonical Connection、tool、风险分类、参数摘要与结果摘要;跨 Agent 汇总,用户随时可查"我的账号被哪个 Agent 做了什么" | 始终开启 |
| 敏感操作通知 | delete 类操作执行后推送一条通知——事后告知,不拦截、不等待 | 开启,可关 |
| Kill switch | 断开 Connection 立即使该 SaaS 全部工具消失并删除其凭据 | 始终可用 |
风险分类(仅作元数据,不驱动拦截)
| 分类 | 判定规则 |
|---|---|
delete | 上游声明 destructiveHint=true,或名称含 delete / remove / purge / archive |
send | 名称含 send / message / notify / reply / comment / post |
read | 仅当 readOnlyHint=true、名称含明确读语义且不含写语义 |
write | 其余全部默认 write |
- 风险分类从上游工具 hint 与名称推导,成本趋近于零;用于审计标注与通知筛选,不用于事前拦截。
- 保留分类元数据意味着未来的团队/企业形态可以在其上叠加策略开关(如"生产库 delete 需管理员批准"),无需重新架构;个人形态默认永不拦截。
- 审计与通知只记录参数摘要、风险与结果摘要;不记录凭据与原始敏感内容。
8. 凭据生命周期
四类凭据、四个落点。任何一类都不进入 Agent 的配置、argv、日志或普通环境。
| 凭据 | 落点 | 续期方式 | Agent 可见性 |
|---|---|---|---|
| SaaS Token(MCP 路径) | 云端 Credential Vault 密文 | refresh lease(见第 9 节) | 永不可见 |
| SaaS Token(CLI 路径) | 本机 User Vault | Provider 各自的 refresh 机制,全部在本机完成 | 永不可见(proxy 注入) |
| 短期 Gateway Token(约 1 小时) | Forwarder token 文件(agent-connectors UID 私有) | 长期续期凭据定时换取,apply 只写该文件 | 永不可见(逐请求注入) |
| 长期续期凭据(最长 90 天) | root-owned systemd credential | 到期前由可信编排器 rebind | 永不可见 |
短期 Token 续期链
- fetch 服务以独立动态 UID 持长期续期凭据向 Gateway 换取短期 Token 响应;apply 以
agent-connectorsUID 把新 Token 写入 Forwarder token 文件。全链路不触碰 Agent。 - 续期使用稳定 requestId 与 CAS,容忍响应丢失后的幂等重试。
- 会话不中断要求:Gateway 的 MCP session 所有者必须仅由
userId + hostId + instanceId + credentialGeneration构成,不含 tokenId;续期只换 tokenId、不换 generation,因此进行中的 MCP session 跨续期继续有效(已实验证实,见第 13 节 E6)。 - 旧短期 Token 在新 Token 生效后立即失效(服务端只认实例当前 tokenId)。
9. Remote MCP 上游合同要点
remote_mcp 路径把 SaaS 官方 Hosted MCP 纳入平台。以下是实现必须满足的合同要点;Catalog 中的 endpoint 与 origin allowlist 是信任根。
OAuth 发现与信任链
- 发现链固定:Catalog protected-resource metadata URL → RFC 9728 Protected Resource Metadata →
authorization_servers[0]→ RFC 8414 Authorization Server Metadata → authorization / token / registration endpoint。 - Catalog 固定两层 origin allowlist(Authorization Server origin 与 OAuth endpoint origin);所有 discovered endpoint 必须 HTTPS 且命中 allowlist。
- OAuth start 与 callback 都必须重新执行发现,且 callback 侧结果必须与 state 中保存的一致;metadata 请求禁止跟随 redirect。
- 始终发送 PKCE S256。metadata、DCR、token、identity 请求统一 20 秒超时、1 MiB 响应上限。
- Client 策略两种:
configured_only(部署方预配置 Client ID/Secret,如 Slack、Box)与dynamic_if_unconfigured(未配置时走 DCR,如 Supabase、Atlassian;临时 Client Secret 立即加密入 Vault,拒绝/过期必须清理)。 - Connection 必须有稳定
externalAccountId(如 SlackteamId:userId、Box/users/me的 id);identity 查询失败则整个 callback 失败,不得创建无稳定身份的 Connection。同 User 多账号按稳定 ID 新增,不得互相覆盖。
凭据并发:CAS 与 refresh lease
- 同一加密 credential 的 Token 刷新、refresh lease 与工具快照都通过
getVersioned + compareAndSwap修改;禁止无条件整体覆盖。 - refresh lease 必须在上游 I/O 前通过 CAS 获得——并发请求只允许一个使用可能单次轮换的 refresh token;等待者观察到 Token lineage 变化后直接采用 winner。
- 访问 Token 距到期不足 60 秒预刷新;上游 401 允许一次强制刷新加单次重试。lease TTL 30 秒;连续 16 次 CAS 冲突后失败关闭。
- snapshot writer 与 refresh writer 互不覆盖对方的并发写入。
上游 MCP client 与预算
| 预算 | 值 | 超限行为 |
|---|---|---|
| 每请求 Remote Connection 数 / 并发 discovery | 20 / 4 | 不暴露 Remote 工具 / 同 deadline 内排队 |
| 每 Connection 工具数 / 每请求全部工具数 | 200 / 500 | 拒绝该集合 / 不暴露 Remote 工具 |
| 快照 JSON / 单响应大小 | 256 KiB / 2 MiB | 不保存 / 失败关闭 |
| 快照 fresh TTL / stale fallback 窗口 | 5 分钟 / 60 分钟 | 到期 live discovery / 之后拒绝 |
| discovery 与 call 绝对 deadline / 上游 subrequest | 20 秒 / 40 | 超时停止 / 失败关闭不扇出 |
- 每次上游操作创建独立 MCP client 与 session,不跨 User、Connection 或请求复用;协商协议
2025-06-18(fallback2025-03-26);SSE 增量解析并在取到匹配 id 后立即取消。 - 工具快照保存在对应 Connection 的加密 credential 内,不得跨 User 或账号共享。
- 工具名只接受
[A-Za-z0-9_.:/-](≤120,加前缀后 ≤128);平台附加参数为accountId(多账号选择),与上游冲突时用_connector_account_idfallback,双冲突则该工具失败关闭;平台参数调用上游前必须剥离。 - 多账号暴露同名工具时,仅 schema、风险与参数选择完全一致才聚合,否则隐藏。
- 上游错误对 Agent 归一化,不回显 Bearer Token、OAuth 响应或内部 endpoint。
10. 架构决策记录
两个关键架构决策及其依据,按长期质量(干净的职责边界、可独立迭代、不产生日后要偿还的过渡形态)选定。
决策:Forwarder 作为平台自有常驻服务进入 Host Runtime,以 agent-connectors UID 运行,仅绑定 loopback(默认端口 14323),单服务多实例路由。
不与凭据 Vault 合并:本机凭据 Vault 选用外部成熟组件(版本与 hash 锁定),fork 扩展它意味着永久的上游维护负担。且二者变更节奏不同——Vault 管 SaaS 凭据存储与 CLI 凭据注入,应当稳定、少变、可审计;Forwarder 管 MCP 传输与短期 Token 注入,随 MCP 协议快速迭代。节奏不同的东西不放在同一个进程里。
安全模型:短期 Token 文件归 agent-connectors UID,Agent UID 完全不可读;Agent 只能通过 loopback 端点使用能力。接入 URL 为不可枚举的 capability 路径 /i/<bindingId>/mcp,泄露时以 rebind 轮换。
被否选项:扩展外部 Vault 二进制(fork 上游);每实例一个 Forwarder 进程(端口膨胀,无隔离收益——同 Host 同用户)。
决策:云直连形态只以标准 MCP OAuth 交付(Gateway 作为 OAuth protected resource,RFC 9728 metadata + PKCE);不提供"把长期 Token 写进 Agent 配置"的模式。
理由一:配置里带 Token 直接违反本方案核心不变量(Agent 配置无 Secret),且从发布之日起就是要被迁移掉的历史包袱。
理由二:本机 Forwarder 形态覆盖全部本地 Agent 场景;云直连是面向 hosted Agent 的增量能力,没有存量压力,可以一次做对。
理由三:remote_mcp 路径本来就要实现同一套规范的 client 侧(发现链、PKCE、DCR),resource server 侧可镜像同一套测试模式;三家目标 Agent 的 OAuth 客户端支持面已确认(第 13 节 E3)。
11. 实施阶段
从零构建的交付顺序。每阶段独立可发布、可验收;后一阶段不阻塞前一阶段上线。
用户身份、Connection、Catalog、Credential Vault、聚合 /mcp(含 session 与 tools/list_changed)、meta-tools、审计与通知、Portal 与 Connections API(第 4 节)、builtin Provider(Linear / GitHub / Google)。此阶段结束即可用任意 MCP Agent 做内部验收。
按第 9 节合同实现发现链、DCR / confidential client、refresh lease、预算与快照,接入 Slack / Box / Supabase / Atlassian,并逐家完成真实 E2E(授权 → identity → tools/list → 只读调用 → 写操作 → refresh → revoke)。
Host 注册与出站控制通道、本机 User Vault 与 CLI shims/proxy、MCP Forwarder(按第 12 节规格)、续期管线(fetch / apply / rebind)。此阶段结束,本机模式全量可用,Agent 接入合同(一行 URL + 一个 PATH 目录)完整成立。
Gateway 实现 OAuth protected resource 合同(metadata、授权、Token 生命周期),hosted Agent 免 Host Runtime 直连。按 D2 决策,此前不提供任何发 Token 的临时替代。
12. Forwarder 合同规格
本节由可运行原型与 13 项合同测试验证得出(原型脚本见 experiments/agent-integration/)。规格覆盖路由、注册表、Token、头透传、流式与失败语义。
定位与进程模型
| 形态 | Host Runtime 内的第一方常驻服务;单服务多实例路由;无第三方依赖(复用受管 Node.js) |
|---|---|
| UID | agent-connectors——短期 Token 文件对 Agent UID 完全不可读 |
| 监听 | 仅 127.0.0.1,默认端口 14323;非 loopback 绑定必须拒绝启动 |
| 生命周期 | systemd 服务归 Host Runtime 管理;SIGTERM 优雅退出 |
路由
| 路由 | 方法 | 合同 |
|---|---|---|
/i/<bindingId>/mcp | POST / GET / DELETE | bindingId 为 [A-Za-z0-9_-]{16,128} 的不可枚举 capability;其他方法返回 405(带 Allow) |
/healthz | GET | 返回 {status, instances};不得包含 Token、上游地址或任何敏感材料 |
| 其他路径 / 未知 binding | * | 一律不透信息的 404;不得区分"路径不存在"与"binding 不存在",不得触发上游请求 |
实例注册表与 Token 文件
# instances.json(agent-connectors 私有,0600;mtime 变化即热加载,新增 bind 无需重启)
{
"<bindingId>": {
"instanceId": "<AgentInstance id>",
"upstreamUrl": "https://<gateway>/mcp", // 仅允许 HTTPS 或 loopback HTTP
"tokenFile": "/var/lib/agent-connectors/mcp-forwarder/<instance>.token" // 必须绝对路径
}
}
- Token 逐请求从 tokenFile 读取——续期热替换天然生效,无缓存失效问题。
- tokenFile 缺失或为空:503
token_unavailable,fail closed,不得调用上游。 - Token 不得出现在任何响应、日志或错误信息中。
- 续期 apply 的唯一落点就是 tokenFile;不触碰任何 Agent 的配置、state 或进程。
头透传(白名单制,未列出的一律丢弃)
| 方向 | 白名单 | 硬性规则 |
|---|---|---|
| 请求 →上游 | content-type、accept、mcp-protocol-version、mcp-session-id、last-event-id | 客户端 Authorization 永不透传;Authorization 始终由 Forwarder 注入 Bearer <当前Token> |
| 响应 →Agent | content-type、mcp-session-id、cache-control、x-sider-gateway-duration-ms | 上游其余头(含任何敏感头)不得回流 |
流式与失败语义
- 上游响应头到达即 flush 给 Agent——SSE 无事件期 Agent 的 fetch 会一直等 headers(实验实证的关键实现要点)。
- 响应 body 逐 chunk 透传,禁止整体缓冲;JSON 与 SSE 同一条路径。
- Agent 断开连接必须 abort 对上游的请求,不留悬挂连接。
- 上游不可达:502
upstream_unreachable,不泄漏上游细节;内部异常:500 归一化。
合同测试清单(原型已全部跑通,实现必须逐项覆盖)
| # | 断言 |
|---|---|
| 1 | 仅 loopback 绑定;非 loopback host 拒绝启动 |
| 2 | 逐请求注入当前 Token;客户端 Authorization 与白名单外的头被剥离 |
| 3 | session / protocol 头双向透传;上游额外头不回流 |
| 4 | Token 文件热替换后,下一请求即用新值(无重启、同 session 继续) |
| 5 | SSE 增量透传,且事件到达前 headers 已 flush |
| 6 | 未知 binding:不透信息 404,且不触发上游请求 |
| 7 | registry 变更后新 binding 免重启可用(mtime 热加载) |
| 8 | Token 缺失/为空:503 fail closed,不触上游,响应无泄漏 |
| 9 | 上游不可达:502 归一化,无上游细节 |
| 10 | DELETE session 透传(session 清理路径完整) |
| 11 | healthz 不含 Token / 上游材料 |
| 12 | registry 校验拒绝:非法 bindingId、非 loopback 明文 HTTP 上游、相对路径 tokenFile |
| 13 | 非法 JSON registry:拒绝加载并保持 fail closed |
13. 实验验证记录
方案的关键假设已用可运行原型与真实 Agent CLI 验证(环境:本地 Gateway + 约 100 行 loopback Forwarder 原型;脚本与复现步骤见下)。
已验证(2026-07-29 本机实测)
| # | 结论 | 证据 |
|---|---|---|
| E1 | Forwarder 透传成立:initialize / Mcp-Session-Id 双向透传 / tools/list(21 工具)/ tools/call / GET SSE / DELETE 全通过;两家真实 Agent 亲测:OpenClaw probe 拉到全部工具,Claude Code 真实会话(claude -p --mcp-config)列出全部工具并成功端到端调用 | 模拟客户端 10/10 + OpenClaw 2026.7.1 probe + Claude Code 2.1.220 真实会话 |
| E1-SSE | SSE 为增量透传非整体缓冲;实现要点:上游响应头到达即 flush,否则 Agent 的 fetch 挂起等待 headers | 500ms 间隔 3 事件分 3 批到达,首尾间隔 999ms |
| E6 | Token 热替换 Agent 无感:注入为逐请求(坏 Token 立即 401);恢复后同 session 继续可用——前提是 session 所有者不含 tokenId(已写入第 8 节要求) | 实测 + 参考实现审读 |
| E4 | 三家官方配置命令均可写入"URL-only 无 Secret"配置,幂等语义差异见第 5 节表 | Claude Code 2.1.220 / Codex 0.144.6 / OpenClaw 2026.7.1 隔离实测 |
| E5 | OpenClaw exec 默认有效策略为 security=full, ask=off,approvals 文件默认不存在——默认安装下仅 PATH 注入即可运行 shim CLI,无需平台写任何 approval。收紧为 allowlist 的部署:一条官方命令 openclaw approvals allowlist add --agent '*' <glob> 由用户/安装脚本自助添加。PATH 注入官方落点 openclaw config set tools.exec.pathPrepend 实测热生效("No gateway restart needed") | 隔离 dev gateway 实测 exec-policy show / approvals get / config set;tools.exec schema 审读 |
| E2 部分 | Gateway 声明 tools.listChanged: true 与 meta-tools 形态均已跑通 | initialize 响应 |
| E3 部分 | 三家 CLI 均有 MCP OAuth 支持面:Claude Code --client-id/--callback-port;Codex mcp login;OpenClaw --auth oauth + mcp login | CLI 命令面 |
待验证(发布前完成)
| # | 内容 | 方法 | 影响 |
|---|---|---|---|
| E5 冒烟 | OpenClaw 真实 LLM turn 端到端执行 PATH 中的 shim CLI(策略层结论已落定) | staging 配真实模型跑一次 exec | 回归项 |
| E2 实时 | 三家 Agent 会话中对 list_changed 的实际感知时机 | 工具集合可变的测试 server + 真实 Agent 会话 | meta-tools UX 文案 |
| E3 实流 | 三家对 OAuth-protected MCP endpoint 的真实授权流 | 搭 OAuth 测试端点逐家跑 | 云直连(阶段 4)排期 |
| E6 签名Token | 生产签名 Token 下新 tokenId 续期的端到端验证 | staging 全链路 | Forwarder 上线前回归项 |
| E7 | 多 remote Connection 聚合 tools/list 冷/热延迟 | staging 连 4+ Provider 实测 | 预算调参 |
| E8 | 授权 URL 在各 Agent 的呈现体验 | request_connection 真实会话观察 | 文案 |
复现步骤
# 1. 起本地 Gateway(任何实现了第 6 节 /mcp 合同的 Gateway 均可) npm run dev # 2. 起 Forwarder 原型(默认 127.0.0.1:14330 → 127.0.0.1:8787/mcp) cd experiments/agent-integration printf '%s' "$DEV_AGENT_TOKEN" > current-token TOKEN_FILE=./current-token node forwarder.mjs # 3. E1+E6:透传与 Token 热替换(10 项断言) TOKEN_FILE=./current-token GOOD_TOKEN="$DEV_AGENT_TOKEN" node e1-e6-client.mjs # 4. E1-SSE:增量透传(模拟 SSE 上游 :14340) TOKEN_FILE=./current-token UPSTREAM=http://127.0.0.1:14340/ node forwarder.mjs & node e1-sse-incremental.mjs # 5. E4:官方配置命令(隔离,不污染真实配置) claude mcp add --scope project --transport http connectors http://127.0.0.1:14330/mcp # 空目录内 CODEX_HOME=$(mktemp -d) codex mcp add connectors --url http://127.0.0.1:14330/mcp openclaw --profile e4test mcp add connectors --url http://127.0.0.1:14330/mcp --transport streamable-http
14. 验收标准(发布门禁)
- 三家目标 Agent 各自只用第 5 节的官方命令 + 一行 URL 完成接入,配置文件中无任何 Secret。
- 任一 Agent 上:连接新 SaaS 后不改配置即可在(至多下一次)tools/list 中看到新工具并成功调用。
- 续期发生时:Agent 配置零变更、Agent 进程零重启、进行中的 MCP session 不中断(签名 Token,staging 验证)。
- Native CLI 在仅 PATH 注入下可用:OpenClaw 用官方
config set tools.exec.pathPrepend;allowlist 部署由用户官方命令自助添加;staging 完成一次真实 LLM turn 的 exec 冒烟。 - Forwarder 通过第 12 节全部 13 项合同测试。
- meta-tools 对话内连接闭环:未连接 → 授权 URL → 浏览器授权 → connected → 调用成功。
- Portal 三页面全流程可用(目录 → 授权落地 → 管理/断开),覆盖第 4 节状态机全部状态。
- 接入方仅用 Connections API(不依赖 Portal)即可实现完整连接管理界面:列表、发起授权、轮询、断开、审计摘要。
- 每个 remote_mcp Provider 完成真实 E2E:授权、稳定 identity、tools/list、只读调用、写操作、refresh rotation、revoke。
- 两个 User 交叉验证:Connection、Token、工具快照、审计记录互不可见。
- 第 8 节凭据落点表逐条复核:每类凭据只出现在声明的落点,Agent 侧无任何凭据痕迹(配置、argv、日志、普通环境)。
15. 安全边界
- 任何 SaaS Token 与短期 Gateway Token 不进入 Agent 配置、argv、日志或普通环境
- Forwarder 与 proxy 仅 loopback,Host 无公网监听
- 短期 Token 文件归
agent-connectorsUID,Agent UID 不可读
- User / Host / Instance 三级隔离;一个 User 一台 active Host
- 审计与通知只记摘要不记凭据
- 跨租户访问在鉴权层拒绝并可审计
- Catalog 是 Provider trust root:endpoint、origin allowlist、driver 路由均以其为准
- Agent、Host、用户都不能选择或覆盖 Provider 的执行路径
- 失败一律 fail closed,不做静默降级或 fallback