我们要做的是一个“远程执行触手”系统:
- 某台 VM / 特殊环境里运行
executor - 本机运行
daemon daemon对Codex / Copilot / 其他 agent暴露工具接口daemon通过 Azure Web PubSub 把指令发给远端executorexecutor执行命令、读写文件、流式返回结果- 一个
daemon session可以同时控制多台机器
核心约束:
executor不暴露任何入站端口executor只需要主动出站连到 Web PubSub- agent 侧体验应尽量像“多台远程机器被挂成一组工具”
推荐拆成三层:
Agent <-> Daemon使用MCPDaemon <-> Executor使用Azure Web PubSub + 自定义 JSON 协议控制面 / 注册 / 鉴权v1先由daemon本地持有 Web PubSub 管理权限;v2再拆出独立 cloud control plane
MCP很适合 northbound interface,因为它本来就是“让 agent 发现并调用 tools/resources”的协议。Web PubSub很适合 southbound transport,因为远端机器只要主动出站连上来,不需要公开端口。- 不建议把
ACP作为daemon <-> executor主协议。
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 适配层更合理。
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)]
负责:
- 对 agent 暴露统一接口
- executor 发现、注册、心跳、租约管理
- 根据能力和标签路由请求到目标 executor
- 管理 Web PubSub 连接、group、权限
- 做任务状态机、日志、审计、超时、取消
- 聚合多个 executor 的结果
负责:
- 连接 Web PubSub
- 上报自身元数据和能力
- 接收 daemon 下发的工具调用
- 在本地执行命令 / 文件操作 / 传输
- 将 stdout/stderr/progress/artifacts 流式返回
负责:
- 作为实时双向 transport
- 提供 group pubsub、连接管理、权限控制
- 避免 executor 暴露公网入口
推荐:
- 使用一个预定义 hub:
rexec - 使用动态 groups 做 discovery、direct control、session stream
推荐 group 命名:
discovery所有 executor 启动后加入,用于探活与发现executor:{executorId}单机控制组,只放这一台 executordaemon:{daemonId}daemon 自己的回包组session:{sessionId}某次任务/终端会话专用组tag:{k}:{v}可选,按环境/区域/系统/角色做广播或筛选
说明:
- 不建议把每台机器做成单独 hub,hub 应该是预定义的少量集合
- group 更适合做机器地址、会话地址和标签广播
推荐 daemon 具备两个基础角色,生产模式下再增加一个可选角色:
管理端使用WebPubSubServiceClient或等价 REST API消息端自己也作为一个 Web PubSub client 连上去接入控制端(可选)线上部署时提供 Web PubSubconnectevent handler
这样做的好处:
- daemon 能发管理指令:发消息、加 group、查连接
- daemon 也能像 executor 一样实时订阅结果流
模式区分:
- 开发模式 不依赖 upstream / event handler,daemon 可以完全本地运行
- 生产模式
daemon 线上部署,并为
connect路径提供 event handler,用于 executor 接入控制
executor 只做一件事:
- 以 client 身份主动出站连接 Web PubSub
连接后最少加入:
discoveryexecutor:{executorId}
推荐 executor 使用可靠子协议对应的 client SDK,而不是自己手写原始协议状态机。
daemon 需要知道:
- 当前有哪些 executor 在线
- 每个 executor 的
executorId / hostname / os / arch / tags / version - 它支持哪些能力
- 最近心跳时间
- 当前是否忙碌
executor 连接成功后,向 discovery 发送:
executor.helloexecutor.capabilities
daemon 收到后在本地 registry 中写入 lease。
executor 每 15s 发一次 executor.heartbeat。
daemon 若 45s 未收到心跳,则把 executor 标为 suspect;90s 后标为 offline。
daemon 启动时,向 discovery 广播 discovery.probe。
所有在线 executor 立即回 executor.hello。
这样可以解决 daemon 晚于 executor 启动时的可见性问题。
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
}
}你说希望把 Codex/Copilot 的内置 tools 都实现出来,我建议 v1 不要追这个目标。
起步阶段更合理的做法是只定义少量、稳定、通用的远程原语,再由 daemon 做 northbound 映射。
也就是说:
- southbound:
executor提供稳定 capability - northbound:
daemon对不同 agent 映射成它们容易调用的 tools
P0 的标准只有一个:一定会用到,而且足够通用。
host.info
task.runtask.cancel
fs.statfs.listfs.readfs.writefs.patchfs.search
artifact.putartifact.get
session.opensession.sendsession.resizesession.closesession.statusprocess.listprocess.killgit.statusgit.diffgit.checkoutgit.commitarchive.packarchive.unpackfs.mkdirfs.movefs.deleteartifact.listservice.startservice.stopservice.statuspackage.install
container.pscontainer.execbrowser.openbrowser.screenshotdb.queryk8s.exec
每个 capability 都应满足:
- 参数 schema 明确
- 幂等性边界明确
- 是否需要 PTY 明确
- 是否允许 streaming 明确
- 是否允许取消明确
- 输出可分块
对 v1 来说,推荐把 task.run 作为最核心原语:
- 普通命令执行用它
- 非交互安装脚本用它
- 大多数 agent 操作先靠它完成
- 真正需要 PTY 时,再进入
P1 session.*
这是最重要的结论:
daemon 首先应该实现成一个本地 MCP server。
原因:
- Codex 已支持 MCP
- GitHub Copilot 也支持通过 MCP 扩展工具
- MCP 天然适合“列工具 -> 调工具 -> 返回结构化结果”
推荐保持工具集合稳定,不要“每台机器动态生成一堆工具名”。
工具名稳定,机器选择通过参数传入。
executors.listexecutors.getexecutors.find
remote.runremote.cancel
v1 里,remote.run 默认只面向单个 executor。
如果 agent 需要同时操作多台机器,直接并发发起多个 tool call 即可;daemon 不需要为此额外设计批处理编排接口。
remote.read_fileremote.write_fileremote.patch_fileremote.list_dirremote.search_text
remote.uploadremote.download
remote.system_info
P1 再增加:
remote.open_sessionremote.send_inputremote.close_sessionremote.git_statusremote.git_diff
推荐额外暴露:
executors://inventoryexecutor://{id}/infosession://{id}/transcriptartifact://{id}/metadata
除了 MCP,还可以补一个仅供你自己或 UI 用的本地 HTTP API:
GET /v1/executorsPOST /v1/commandsGET /v1/sessions/{id}POST /v1/sessions/{id}/cancel
但它是次要接口,不是主接口。
不建议在 Web PubSub 里再嵌一个复杂的通用 RPC 框架。
直接定义一层轻量 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 的追踪 IDreplyTo指明回包 group
推荐最少定义这些 type:
discovery.probeexecutor.helloexecutor.heartbeatexecutor.goodbyeexecutor.capabilities
tool.invoketool.canceltool.ack
stream.openstream.chunkstream.close
tool.resulttool.error
artifact.metaartifact.chunkartifact.complete
{
"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
}
}
}{
"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"
}
}{
"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": []
}
}executor 必须按 messageId 去重,避免重连或重发时重复执行 destructive 操作。
stream.chunk 使用单调递增的 seq。
daemon 对同一 requestId + stream 按 seq 重组。
stdout/stderr 超过阈值后:
- 继续发送摘要 chunk
- 全量输出写入 artifact
tool.result只返回 artifact 引用
二进制内容不要直接塞 JSON body,统一走 artifact 通道。
executor不持有 Web PubSub 管理权限executor不持有 access key、connection string 等原始凭据daemon或其附属服务负责接入控制- 不把 Web PubSub connection string 发到远端机器
开发阶段按“最省事但边界清晰”的方式做:
daemon仍然是本地进程- Web PubSub 对 executor 启用 anonymous connect
- executor 启动时不需要任何额外凭据
- daemon 继续持有服务端管理身份,用于 publish / group / 管理
这样做的目的只有一个:先把开发链路跑通。
开发模式下,executor 本地配置只需要:
executorId- Web PubSub endpoint / hub
- 可选的显示名 / 标签
适合:
- 单操作者
- 你自己掌控这些 VM
- 先把链路打通
缺点:
- 没有真正的接入鉴权
- 不适合多人共用或公网暴露
- 只适合开发环境
最终形态可以把 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 就够了。
v1 不做复杂权限控制。
只保留最小必要原则:
- 开发模式下,executor 直接 anonymous connect
- 生产模式下,executor 通过 connect event handler 被放行
- daemon 持有管理权限
- group 采用固定命名约定
- 先不引入按 session 动态 grant/revoke 的复杂机制
等基础链路稳定、确实出现多人共用或多租户需求,再补细粒度权限模型。
核心原则:
- 一个 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
不把“多机并发”设计成特殊协议能力。
v1 的简单模型是:
- 一个
remote.run处理一个目标 executor - 多台机器就是多个独立 tool call
- daemon 只负责把每个 call 路由到对应 executor
- 聚合、等待、是否并发,由上层 agent 自己决定
如果未来确实反复出现“同一条操作要稳定地下发到一批机器”的需求,再增加批量接口;但这不应该进入当前 v1 主路径。
executor 与 daemon 都应支持:
- 自动重连
- 重连后重新上报 hello/capabilities
- 对正在运行但未完成的任务做状态恢复
v1 不做复杂恢复语义。
最小原则:
- 非交互短任务:断线或超时就标失败,由 agent 视情况重试
- 长任务:executor 可以在本地保留最小 task state,但 daemon 不承诺完整恢复流式输出
- PTY / 交互任务:放到
P1 session.*后再单独处理,不进入当前主路径
每一层都要打统一 trace 字段:
traceIdrequestIdsessionIdexecutorIddaemonId
建议最少记录:
- executor connect / disconnect
- hello / heartbeat / timeout
- tool.invoke / ack / result / error
- stream bytes
- artifact 上传下载大小
如果你希望后续和 Codex/Copilot 的本地工具生态更顺滑,推荐:
daemon: TypeScript 或 .NETexecutor: TypeScript、Go 或 Rust
我会优先推荐:
daemon: TypeScriptexecutor: Go
原因:
daemon做 MCP、JSON schema、agent tool mapping,TypeScript 开发效率高executor做进程管理、PTY、文件系统、跨平台部署,Go 更稳
但如果你想降低心智负担,daemon + executor 都用 TypeScript 也完全可行。
mcp-serverexecutor-registryrequest-routerwps-clientwps-adminsession-managerartifact-manager
transportdispatchercapability-manifesttask-runnersession-runnerfs-adapterartifact-storetask-state
- 单 hub
- daemon MCP server
- executor anonymous connect
- executor hello / heartbeat
task.runfs.read/write/list- 单机调用
- stdout/stderr streaming
- selector / tags
- 多机 fan-out
- artifact 通道
- reconnect / resume
session.*git.*
- daemon 线上化部署
connectevent handler- whitelist naming
- 操作记录与回放
- 更完整的 PTY 恢复
最终建议很明确:
daemon -> agent先做MCPdaemon <-> executor先做Web PubSub + 自定义 payloadACP先不要作为主协议,只保留未来适配空间- 能力实现先抓
task.run + fs + artifact,不要一开始追求“完全复制所有内置 tools” - 权限控制先保持
v1最小可用,不提前设计复杂 RBAC 和动态授权 - 开发期直接 anonymous connect,生产期再上线上 daemon +
connectevent handler + naming whitelist
如果按这个方向继续,下一步最值得马上细化的是三件事:
payload schema定稿MCP tool schema定稿executor capability manifest定稿
这三件事一旦定下来,后面写 daemon 和 executor 就会非常顺。