Skip to content

Latest commit

 

History

History
830 lines (590 loc) · 18.7 KB

File metadata and controls

830 lines (590 loc) · 18.7 KB

远程 Executor 架构设计

1. 目标

我们要做的是一个“远程执行触手”系统:

  • 某台 VM / 特殊环境里运行 executor
  • 本机运行 daemon
  • daemonCodex / Copilot / 其他 agent 暴露工具接口
  • daemon 通过 Azure Web PubSub 把指令发给远端 executor
  • executor 执行命令、读写文件、流式返回结果
  • 一个 daemon session 可以同时控制多台机器

核心约束:

  • executor 不暴露任何入站端口
  • executor 只需要主动出站连到 Web PubSub
  • agent 侧体验应尽量像“多台远程机器被挂成一组工具”

2. 推荐结论

2.1 总体选型

推荐拆成三层:

  1. Agent <-> Daemon 使用 MCP
  2. Daemon <-> Executor 使用 Azure Web PubSub + 自定义 JSON 协议
  3. 控制面 / 注册 / 鉴权 v1 先由 daemon 本地持有 Web PubSub 管理权限;v2 再拆出独立 cloud control plane

2.2 为什么这么选

  • MCP 很适合 northbound interface,因为它本来就是“让 agent 发现并调用 tools/resources”的协议。
  • Web PubSub 很适合 southbound transport,因为远端机器只要主动出站连上来,不需要公开端口。
  • 不建议把 ACP 作为 daemon <-> executor 主协议。

2.3 对 ACP 的判断

ACP 现在有至少两种你可能指的东西:

  • Agent Communication Protocol 更偏 agent-to-agent 的 REST 互通、能力声明、任务编排
  • Agent Client Protocol 更偏 IDE / client 和 coding agent 之间的协议

它们都不是你这里最关键的问题边界。你这里最难的是:

  • 多 executor 的发现与选路
  • 低延迟命令分发
  • stdout/stderr 流式回传
  • PTY / cancel / chunk / artifact
  • 断线恢复

这些更适合自己定义一层轻量 payload,直接跑在 Web PubSub 上。
如果未来你希望把 daemon 本身也包装成一个“可被其他 agent 平台调用的 agent”,那时再给 daemon 增加 ACP 适配层更合理。


3. 总体架构

flowchart LR
    A[Codex / Copilot / Other Agent] -->|MCP| D[Local Daemon]
    D -->|Service SDK / Client SDK| W[Azure Web PubSub]
    W -->|WebSocket outbound only| E1[Executor #1]
    W -->|WebSocket outbound only| E2[Executor #2]
    W -->|WebSocket outbound only| E3[Executor #N]
    D --> S[(Local Session Store / Cache)]
    D --> O[(Optional Artifact Store)]
Loading

3.1 角色划分

Daemon

负责:

  • 对 agent 暴露统一接口
  • executor 发现、注册、心跳、租约管理
  • 根据能力和标签路由请求到目标 executor
  • 管理 Web PubSub 连接、group、权限
  • 做任务状态机、日志、审计、超时、取消
  • 聚合多个 executor 的结果

Executor

负责:

  • 连接 Web PubSub
  • 上报自身元数据和能力
  • 接收 daemon 下发的工具调用
  • 在本地执行命令 / 文件操作 / 传输
  • 将 stdout/stderr/progress/artifacts 流式返回

Azure Web PubSub

负责:

  • 作为实时双向 transport
  • 提供 group pubsub、连接管理、权限控制
  • 避免 executor 暴露公网入口

4. 通信与拓扑设计

4.1 Hub 与 Group 设计

推荐:

  • 使用一个预定义 hub:rexec
  • 使用动态 groups 做 discovery、direct control、session stream

推荐 group 命名:

  • discovery 所有 executor 启动后加入,用于探活与发现
  • executor:{executorId} 单机控制组,只放这一台 executor
  • daemon:{daemonId} daemon 自己的回包组
  • session:{sessionId} 某次任务/终端会话专用组
  • tag:{k}:{v} 可选,按环境/区域/系统/角色做广播或筛选

说明:

  • 不建议把每台机器做成单独 hub,hub 应该是预定义的少量集合
  • group 更适合做机器地址、会话地址和标签广播

4.2 Daemon 与 Web PubSub 的连接模型

推荐 daemon 具备两个基础角色,生产模式下再增加一个可选角色:

  1. 管理端 使用 WebPubSubServiceClient 或等价 REST API
  2. 消息端 自己也作为一个 Web PubSub client 连上去
  3. 接入控制端(可选) 线上部署时提供 Web PubSub connect event handler

这样做的好处:

  • daemon 能发管理指令:发消息、加 group、查连接
  • daemon 也能像 executor 一样实时订阅结果流

模式区分:

  • 开发模式 不依赖 upstream / event handler,daemon 可以完全本地运行
  • 生产模式 daemon 线上部署,并为 connect 路径提供 event handler,用于 executor 接入控制

4.3 Executor 与 Web PubSub 的连接模型

executor 只做一件事:

  • 以 client 身份主动出站连接 Web PubSub

连接后最少加入:

  • discovery
  • executor:{executorId}

推荐 executor 使用可靠子协议对应的 client SDK,而不是自己手写原始协议状态机。


5. 服务发现机制

5.1 发现目标

daemon 需要知道:

  • 当前有哪些 executor 在线
  • 每个 executor 的 executorId / hostname / os / arch / tags / version
  • 它支持哪些能力
  • 最近心跳时间
  • 当前是否忙碌

5.2 推荐发现流程

启动注册

executor 连接成功后,向 discovery 发送:

  • executor.hello
  • executor.capabilities

daemon 收到后在本地 registry 中写入 lease。

周期心跳

executor 每 15s 发一次 executor.heartbeat
daemon 若 45s 未收到心跳,则把 executor 标为 suspect90s 后标为 offline

主动探测

daemon 启动时,向 discovery 广播 discovery.probe
所有在线 executor 立即回 executor.hello

这样可以解决 daemon 晚于 executor 启动时的可见性问题。

5.3 Registry 结构

daemon 本地维护一份内存 registry,必要时持久化到磁盘:

{
  "executorId": "exec-01",
  "connectionId": "wps-conn-xxx",
  "hostname": "vm-build-01",
  "os": "ubuntu-24.04",
  "arch": "x86_64",
  "tags": {
    "env": "dev",
    "region": "aue",
    "role": "builder"
  },
  "version": "0.1.0",
  "status": "idle",
  "lastSeenAt": "2026-04-21T10:00:00Z",
  "capabilities": {
    "pty": true,
    "shell.exec": true,
    "fs.read": true,
    "fs.write": true,
    "git.basic": true,
    "artifact.upload": true
  }
}

6. Executor 能力模型

你说希望把 Codex/Copilot 的内置 tools 都实现出来,我建议 v1 不要追这个目标。
起步阶段更合理的做法是只定义少量、稳定、通用的远程原语,再由 daemon 做 northbound 映射。

也就是说:

  • southbound:executor 提供稳定 capability
  • northbound:daemon 对不同 agent 映射成它们容易调用的 tools

6.1 P0 必须能力

P0 的标准只有一个:一定会用到,而且足够通用。

主机信息

  • host.info

通用执行

  • task.run
  • task.cancel

Filesystem

  • fs.stat
  • fs.list
  • fs.read
  • fs.write
  • fs.patch
  • fs.search

Transfer / Artifact

  • artifact.put
  • artifact.get

6.2 P1 强烈建议能力

  • session.open
  • session.send
  • session.resize
  • session.close
  • session.status
  • process.list
  • process.kill
  • git.status
  • git.diff
  • git.checkout
  • git.commit
  • archive.pack
  • archive.unpack
  • fs.mkdir
  • fs.move
  • fs.delete
  • artifact.list
  • service.start
  • service.stop
  • service.status
  • package.install

6.3 P2 可选能力

  • container.ps
  • container.exec
  • browser.open
  • browser.screenshot
  • db.query
  • k8s.exec

6.4 设计原则

每个 capability 都应满足:

  • 参数 schema 明确
  • 幂等性边界明确
  • 是否需要 PTY 明确
  • 是否允许 streaming 明确
  • 是否允许取消明确
  • 输出可分块

v1 来说,推荐把 task.run 作为最核心原语:

  • 普通命令执行用它
  • 非交互安装脚本用它
  • 大多数 agent 操作先靠它完成
  • 真正需要 PTY 时,再进入 P1 session.*

7. Daemon 对 Agent 暴露什么接口

7.1 主接口:MCP Server

这是最重要的结论:
daemon 首先应该实现成一个本地 MCP server

原因:

  • Codex 已支持 MCP
  • GitHub Copilot 也支持通过 MCP 扩展工具
  • MCP 天然适合“列工具 -> 调工具 -> 返回结构化结果”

7.2 暴露的 MCP tools

推荐保持工具集合稳定,不要“每台机器动态生成一堆工具名”。
工具名稳定,机器选择通过参数传入。

发现与选择

  • executors.list
  • executors.get
  • executors.find

通用远程执行

  • remote.run
  • remote.cancel

v1 里,remote.run 默认只面向单个 executor。
如果 agent 需要同时操作多台机器,直接并发发起多个 tool call 即可;daemon 不需要为此额外设计批处理编排接口。

文件操作

  • remote.read_file
  • remote.write_file
  • remote.patch_file
  • remote.list_dir
  • remote.search_text

资产传输

  • remote.upload
  • remote.download

诊断

  • remote.system_info

P1 再增加:

  • remote.open_session
  • remote.send_input
  • remote.close_session
  • remote.git_status
  • remote.git_diff

7.3 MCP resources

推荐额外暴露:

  • executors://inventory
  • executor://{id}/info
  • session://{id}/transcript
  • artifact://{id}/metadata

7.4 可选的 HTTP Admin API

除了 MCP,还可以补一个仅供你自己或 UI 用的本地 HTTP API:

  • GET /v1/executors
  • POST /v1/commands
  • GET /v1/sessions/{id}
  • POST /v1/sessions/{id}/cancel

但它是次要接口,不是主接口。


8. Web PubSub 内部 Payload 协议

不建议在 Web PubSub 里再嵌一个复杂的通用 RPC 框架。
直接定义一层轻量 envelope 即可。

8.1 Envelope

{
  "v": 1,
  "type": "tool.invoke",
  "messageId": "msg_01J...",
  "requestId": "req_01J...",
  "sessionId": "sess_01J...",
  "traceId": "trace_01J...",
  "from": {
    "kind": "daemon",
    "id": "daemon-local-01"
  },
  "to": {
    "kind": "executor",
    "id": "exec-01"
  },
  "ts": "2026-04-21T10:00:00.000Z",
  "timeoutMs": 300000,
  "replyTo": {
    "group": "session:sess_01J..."
  },
  "body": {}
}

字段约定:

  • messageId 单条消息 ID,用于去重
  • requestId 一个调用链路的主键
  • sessionId 终端会话/任务会话 ID,可选
  • traceId 跨 daemon / executor / artifact store 的追踪 ID
  • replyTo 指明回包 group

8.2 消息类型

推荐最少定义这些 type:

发现与租约

  • discovery.probe
  • executor.hello
  • executor.heartbeat
  • executor.goodbye
  • executor.capabilities

控制调用

  • tool.invoke
  • tool.cancel
  • tool.ack

流式输出

  • stream.open
  • stream.chunk
  • stream.close

结果与错误

  • tool.result
  • tool.error

资产

  • artifact.meta
  • artifact.chunk
  • artifact.complete

8.3 tool.invoke 示例

{
  "v": 1,
  "type": "tool.invoke",
  "requestId": "req_01JABC",
  "sessionId": "sess_01JABC",
  "from": { "kind": "daemon", "id": "daemon-01" },
  "to": { "kind": "executor", "id": "exec-01" },
  "replyTo": { "group": "session:sess_01JABC" },
  "body": {
    "tool": "task.run",
    "args": {
      "command": "apt-get update && apt-get install -y nginx",
      "cwd": "/root",
      "env": {},
      "timeoutMs": 1800000,
      "pty": false
    }
  }
}

8.4 stream.chunk 示例

{
  "v": 1,
  "type": "stream.chunk",
  "requestId": "req_01JABC",
  "sessionId": "sess_01JABC",
  "from": { "kind": "executor", "id": "exec-01" },
  "seq": 12,
  "body": {
    "stream": "stdout",
    "encoding": "utf-8",
    "data": "Get:1 http://archive.ubuntu.com/ubuntu ...\n"
  }
}

8.5 tool.result 示例

{
  "v": 1,
  "type": "tool.result",
  "requestId": "req_01JABC",
  "sessionId": "sess_01JABC",
  "from": { "kind": "executor", "id": "exec-01" },
  "body": {
    "ok": true,
    "exitCode": 0,
    "durationMs": 52431,
    "artifacts": []
  }
}

8.6 重要规则

去重

executor 必须按 messageId 去重,避免重连或重发时重复执行 destructive 操作。

顺序

stream.chunk 使用单调递增的 seq
daemon 对同一 requestId + streamseq 重组。

大输出

stdout/stderr 超过阈值后:

  • 继续发送摘要 chunk
  • 全量输出写入 artifact
  • tool.result 只返回 artifact 引用

二进制

二进制内容不要直接塞 JSON body,统一走 artifact 通道。


9. 权限与安全模型

9.1 基本原则

  • executor 不持有 Web PubSub 管理权限
  • executor 不持有 access key、connection string 等原始凭据
  • daemon 或其附属服务负责接入控制
  • 不把 Web PubSub connection string 发到远端机器

9.2 v1 方案

开发阶段按“最省事但边界清晰”的方式做:

  • daemon 仍然是本地进程
  • Web PubSub 对 executor 启用 anonymous connect
  • executor 启动时不需要任何额外凭据
  • daemon 继续持有服务端管理身份,用于 publish / group / 管理

这样做的目的只有一个:先把开发链路跑通。

开发模式下,executor 本地配置只需要:

  • executorId
  • Web PubSub endpoint / hub
  • 可选的显示名 / 标签

适合:

  • 单操作者
  • 你自己掌控这些 VM
  • 先把链路打通

缺点:

  • 没有真正的接入鉴权
  • 不适合多人共用或公网暴露
  • 只适合开发环境

9.3 v2 方案

最终形态可以把 daemon 部署到线上,并在同一个服务里挂 Web PubSub connect event handler。

推荐模型:

  • executor 启动时携带一个预定义的 name 或 naming 前缀
  • daemon 维护允许接入的 whitelist naming 规则
  • Web PubSub 收到 executor 的 connect 事件后,调用 daemon 的 event handler
  • event handler 根据 naming whitelist 决定 accept / reject
  • accept 后,由 event handler 为连接写入规范化的 userId、groups 或初始 metadata

这样 executor 侧不需要保存任何原始权限材料;它只声明“我是谁”,接入是否成立由 daemon 侧判定。

这个模式更适合:

  • daemon 长期在线
  • executor 安装规模增大
  • 需要统一管控哪些机器允许接入

如果后续规模继续扩大,再额外拆出独立 control plane 也可以;但在你当前场景里,先把 connect event handler 放进线上 daemon 就够了。

9.4 细粒度权限

v1 不做复杂权限控制。
只保留最小必要原则:

  • 开发模式下,executor 直接 anonymous connect
  • 生产模式下,executor 通过 connect event handler 被放行
  • daemon 持有管理权限
  • group 采用固定命名约定
  • 先不引入按 session 动态 grant/revoke 的复杂机制

等基础链路稳定、确实出现多人共用或多租户需求,再补细粒度权限模型。


10. 执行模型

10.1 一次普通命令

核心原则:

  • 一个 tool call 对应一个独立 request
  • agent 是否等待返回,是这个 tool call 自身的语义
  • 如果 agent 并发发起多个 tool call,daemon 就并发转发多个 request
  • Web PubSub 本身是解耦消息通道,不存在“前一个 request 必须等 response 回来才能发下一个”的协议限制
sequenceDiagram
    participant A as Agent
    participant D as Daemon
    participant W as Web PubSub
    participant E as Executor

    A->>D: remote.run(target=exec-01, command=...)
    D->>D: resolve executor + allocate requestId
    D->>W: publish tool.invoke
    W->>E: tool.invoke
    E->>W: tool.ack
    E->>W: stream.chunk(stdout/stderr)
    E->>W: tool.result
    W->>D: ack/chunks/result
    D->>A: MCP streamed result / final result
Loading

10.2 多机并发

不把“多机并发”设计成特殊协议能力。
v1 的简单模型是:

  • 一个 remote.run 处理一个目标 executor
  • 多台机器就是多个独立 tool call
  • daemon 只负责把每个 call 路由到对应 executor
  • 聚合、等待、是否并发,由上层 agent 自己决定

如果未来确实反复出现“同一条操作要稳定地下发到一批机器”的需求,再增加批量接口;但这不应该进入当前 v1 主路径。


11. 错误处理与恢复

11.1 断线

executor 与 daemon 都应支持:

  • 自动重连
  • 重连后重新上报 hello/capabilities
  • 对正在运行但未完成的任务做状态恢复

11.2 任务恢复策略

v1 不做复杂恢复语义。

最小原则:

  • 非交互短任务:断线或超时就标失败,由 agent 视情况重试
  • 长任务:executor 可以在本地保留最小 task state,但 daemon 不承诺完整恢复流式输出
  • PTY / 交互任务:放到 P1 session.* 后再单独处理,不进入当前主路径

12. 可观测性

每一层都要打统一 trace 字段:

  • traceId
  • requestId
  • sessionId
  • executorId
  • daemonId

建议最少记录:

  • executor connect / disconnect
  • hello / heartbeat / timeout
  • tool.invoke / ack / result / error
  • stream bytes
  • artifact 上传下载大小

13. 实现建议

13.1 语言建议

如果你希望后续和 Codex/Copilot 的本地工具生态更顺滑,推荐:

  • daemon: TypeScript 或 .NET
  • executor: TypeScript、Go 或 Rust

我会优先推荐:

  • daemon: TypeScript
  • executor: Go

原因:

  • daemon 做 MCP、JSON schema、agent tool mapping,TypeScript 开发效率高
  • executor 做进程管理、PTY、文件系统、跨平台部署,Go 更稳

但如果你想降低心智负担,daemon + executor 都用 TypeScript 也完全可行。

13.2 模块拆分

daemon

  • mcp-server
  • executor-registry
  • request-router
  • wps-client
  • wps-admin
  • session-manager
  • artifact-manager

executor

  • transport
  • dispatcher
  • capability-manifest
  • task-runner
  • session-runner
  • fs-adapter
  • artifact-store
  • task-state

14. 分阶段落地

Phase 1: 跑通最小链路

  • 单 hub
  • daemon MCP server
  • executor anonymous connect
  • executor hello / heartbeat
  • task.run
  • fs.read/write/list
  • 单机调用
  • stdout/stderr streaming

Phase 2: 做好多机与可靠性

  • selector / tags
  • 多机 fan-out
  • artifact 通道
  • reconnect / resume
  • session.*
  • git.*

Phase 3: 做强控制面

  • daemon 线上化部署
  • connect event handler
  • whitelist naming
  • 操作记录与回放
  • 更完整的 PTY 恢复

15. 最终建议

最终建议很明确:

  1. daemon -> agent 先做 MCP
  2. daemon <-> executor 先做 Web PubSub + 自定义 payload
  3. ACP 先不要作为主协议,只保留未来适配空间
  4. 能力实现先抓 task.run + fs + artifact,不要一开始追求“完全复制所有内置 tools”
  5. 权限控制先保持 v1 最小可用,不提前设计复杂 RBAC 和动态授权
  6. 开发期直接 anonymous connect,生产期再上线上 daemon + connect event handler + naming whitelist

如果按这个方向继续,下一步最值得马上细化的是三件事:

  1. payload schema 定稿
  2. MCP tool schema 定稿
  3. executor capability manifest 定稿

这三件事一旦定下来,后面写 daemon 和 executor 就会非常顺。