dsh-remote-fleet
v0.5.2
Published
Drive remote DeepSeek Harness / Codex agents over ACP: machine registry, workstream routing, escalation policy, direct chat.
Maintainers
Readme
dsh-remote-fleet
让主机 DSH 通过 SSH + ACP 给远程机器上的 DSH 派活、续会话、处理审批。 一个插件,十个工具,没有额外的 HTTP 服务、没有端口、没有证书。
主机 DSH(调度)
│ SSH(传输)
│ ACP(会话:session/new · resume · prompt · permission)
▼
pc4090 上的 `dsh --profile acp`它做什么
- 机器注册:记住每台机器的 host、用户、默认目录、标签。
- 委托:
fleet_delegate立即返回 handle,后台跑;fleet_wait跟进。 - 会话路由:优先用工作流绑定,其次找同目录下最近的、标题相关的会话,最后才新建。
- 上下文胶囊:每次委托都带任务、约束、上文,即使路由错了也能把任务拉回来。
- 升级策略:不可逆操作(删库、force push、动 schema、碰密钥…)一律停下来问人,其余自动放行。
- 可审计:返回里带
mode、route_reason、candidates,走错了能一眼看出来。
它不做什么
- 不接 Codex(v2 加适配器;本轮只做 ACP)。
- 不做客户端 UI(工具 + 对话已经能跑通)。
- 不做多用户权限(假设单人多机)。
- 不管远程 DSH 的安装和登录(前置条件,见下)。
前置条件
主机(跑 DSH 的那台)
- DSH 能加载第三方 bundle(
dsh plugin可用)。 ssh可用,能免密登录到目标机器。
远程机器(每台)
装了 DSH,且
dsh --profile acp能启动:ssh pc4090 'cd ~/work/app && dsh --profile acp' # 什么都不打印、进程挂着 = 正常:stdout 只走 ACP 协议SSH key 已配置(
BatchMode=yes能登录)。工作目录存在且是绝对路径。
建议用 Tailscale 组网,主机和远程都在同一个 tailnet。
接入机器的三种形态
插件只认一件事:那台机器上有一条命令能说 ACP。装的是 DSH 还是 Codex,对插件是透明的。
| 形态 | 装什么 | command |
|---|---|---|
| DSH | 桌面 App 或 CLI(自带 ACP server) | dsh --profile acp |
| Codex | @agentclientprotocol/codex-acp(Codex 的 ACP 适配器) | codex-acp |
| WSL 里的 DSH / Codex | 在发行版里装上面任意一种 | 同左,但 SSH 要进到发行版里 |
Codex 机器
Codex 自己没有 ACP server,需要一个适配器。官方维护的是
@agentclientprotocol/codex-acp(由 Codex App Server 驱动),另有一个 Go 单文件的
codex-acp(要求 Codex CLI 在 PATH)。
# 在 Codex 那台机器上
npm install -g @agentclientprotocol/codex-acp
codex login # 已有登录状态就跳过
which codex-acp # 记下绝对路径
# 自检:挂住、不打印任何东西 = 正常
codex-acp注册:
{
"id": "pc-codex",
"host": "pc4090",
"kind": "codex",
"cwd": "/home/luis/work/app",
"command": "/home/luis/.npm-global/bin/codex-acp"
}对话里说「把 pc4090 加进来,用 codex,命令是 …」即可。
它会复用 ~/.codex 的登录态和会话(rollout),所以 fleet_sessions 能看到你之前在 Codex 里聊过的会话,session/resume 也能接上。工具审批会走 ACP 的 session/request_permission,和 DSH 机器完全同一条升级策略。
两个版本差异:官方包自带
@openai/codex依赖,装完体积大但省事;Go 版要求你机器上已经有 Codex CLI,并且用codex-acp login登录。
WSL2 机器
推荐顺序:
A. 直接 SSH 进发行版(最干净)
在 WSL 里跑自己的 sshd,端口 2222,主机侧做端口转发(或 Win11 的 mirrored 网络):
# WSL 内
sudo apt install openssh-server
sudo sed -i 's/^#\?Port .*/Port 2222/' /etc/ssh/sshd_config
sudo service ssh restart主 DSH 上:
ssh -p 2222 luis@wsl-host 'cd /home/luis/work/app && codex-acp'注册时 host: "wsl-host", port: 2222, cwd: "/home/luis/work/app"。对插件来说这就是一台普通 Linux 机器,没有任何特殊处理。
B. Windows OpenSSH + 默认 shell 改成 wsl.exe
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
-Value "C:\Windows\System32\wsl.exe" -PropertyType String -Force之后 ssh winbox 直接落在发行版里,cd /home/... && codex-acp 正常工作。
C. Windows OpenSSH + 包装命令(不改默认 shell)
Windows 的 cmd 不认 POSIX 的 cd '...',所以让命令自己 cd,并关掉前缀:
{
"id": "win-wsl",
"host": "winbox",
"kind": "codex",
"cwd": "/home/luis/work/app",
"cwd_prefix": false,
"command": "wsl.exe -d Ubuntu -- bash -lc \"cd /home/luis/work/app && exec codex-acp\""
}cwd_prefix: false 时插件只发 exec <command>,cd 由你负责。
WSL 的四个坑
- 空闲会停:WSL 没有前台进程会被回收,SSH 直接连不上。要么在发行版里挂个常驻进程,要么接受冷启动失败后重试。
- 别用
/mnt/c:跨文件系统 IO 慢十倍、权限语义也不对。工作目录放/home/<user>/work。 - Tailscale 要装在发行版里:装 Windows 侧的话,宿主机进不来(除非 mirrored 网络)。
- 登录态独立:WSL 里的
codex login和 Windows 侧的~/.codex不是一份。
Windows 原生机器
Windows 上也能当 worker(DSH 有 Windows 构建),但同样要 cwd_prefix: false,命令用 PowerShell 包一层:
{
"command": "powershell -NoProfile -Command \"Set-Location C:\\work\\app; codex-acp\"",
"cwd_prefix": false
}安装
dsh plugin --profile web add /path/to/plugins/dsh-remote-fleetdsh plugin 会初始化 profile(首次)、把包 link 进去,并把它加进 dsh.profile.bundles。
验证:
dsh --profile web --dump-config | grep -A2 remote-fleet
# # == dsh-remote-fleet
# - id: remote-fleet
# name: dsh-remote-fleet重启 DSH,模型就能看到 fleet_* 工具。
本地开发时
add一个目录会生成link:依赖,改代码后重启 DSH 即可生效。
用起来是什么样
你:把 pc4090 加进来,用户 luis,默认目录 /home/luis/work/app
DSH:好,加好了。连一下试试?
→ fleet_add_machine {id:"pc4090", host:"pc4090", user:"luis", cwd:"/home/luis/work/app"}
→ fleet_health {machine:"pc4090"}
你:去 4090 上把登录重构收尾,补个 e2e
DSH:→ fleet_delegate {machine:"pc4090", task:"收尾登录重构,补 e2e", workstream:"login-refactor"}
← {delegate_id:"dl-3f2a", status:"queued"}
→ fleet_wait {delegate_id:"dl-3f2a", timeout_ms:60000}
...远程 agent 要删东西时,fleet_wait 会返回:
{
"status": "needs_input",
"question": {
"kind": "permission",
"risk": "irreversible",
"reason": "recursive delete",
"title": "rm -rf build",
"options": [{ "optionId": "allow-once", "kind": "allow_once" }, ...]
}
}主机 DSH 把这个问题转给你,你回一句“允许”,它调:
{ "delegate_id": "dl-3f2a", "decision": "allow" }原生面板「远程机器」
装好之后,DSH 侧边栏会出现一个 远程机器 面板(客户端插件,dsh.client),
它直接长在 DSH 的界面里,不是外挂网页:
- 机器列表:连接状态、类型、主机、工作目录
- 对话:点机器卡片上的「对话」进入——新会话 / 历史会话 / 直接发消息 / 停止
- 授权卡片:远程 agent 要跑危险命令时,面板里直接出「允许 / 拒绝」
- 添加机器:SSH → 探测 → 目录选择 → 保存,四步向导
- 删除机器、断开连接、重新检测都在卡片上
对话走的是同一套 ACP:session/new 或 session/load(回放历史),
每次发送就是一次 session/prompt,session/update 的增量文本按 800ms 轮询回显。
和 fleet_delegate 用的是同一个连接,互不干扰。
面板设置与状态在
~/.dsh/remote-fleet/state.json。
对话里的工具卡片
fleet_* 工具在主机对话里不再显示成一坨 JSON,而是渲染成一张卡片:
◆ 远程 Agent [商品组] [完成] 112 步 · 152.3s
介绍你这个工作区(D:\商品组)是干什么的…
会话 4149492f · new
─────────────────────────────────────────
结果:这是商品组围绕灵云智本体平台做航空客户工作台/数字员工的…实现分两层:
- host:每个工具声明
output.render(紧凑文本)和output.presentationMeta(给卡片的结构化字段:agent、状态、会话、步数、耗时、待决定的问题)。 - client:按工具名注册
tool.call.toolview插槽:
ctx.slots.register({ name: "tool.call.toolview", key: "fleet_delegate" }, FleetToolCard)卡片读取 block.meta(就是 host 的 presentationMeta)、block.argsRaw、
block.content 和 block.isError,所以运行中 / 完成 / 失败三种状态都能画。
独立页面(备用)
同样一套能力也注册在 DSH 的 Web 服务上,浏览器可以直接开:
<DSH 的 origin>/remote-fleet/能力:
- 机器卡片列表:连接状态、类型、主机、工作目录、标签
- 每台机器:连接 / 重新检测 / 看会话 / 断开 / 删除
- 添加向导四步:填 SSH → 探测环境 → 选目录 → 保存并连接
- 探测会告诉你目标机器缺什么;缺
codex-acp可以在页面上后台安装并看日志 - 目录浏览器直接列目标机器的文件系统(走 SSH,不需要额外服务)
让模型给你链接:fleet_ui。
鉴权沿用 DSH 的 cookie。如果打开是 401,先在同一个浏览器里打开一次 DSH 启动时打印的 带
?token=的地址,再访问/remote-fleet/。
工具
| 工具 | 作用 |
|---|---|
| fleet_probe | 只用 SSH 探测:系统、node、codex-acp / dsh 是否存在、Codex 是否登录 |
| fleet_browse | 列目标机器的目录,用于选工作区 |
| fleet_bootstrap | 在目标机器后台安装 codex-acp(立即返回,轮询状态) |
| fleet_bootstrap_status | 看安装进度与日志 |
| fleet_ui | 返回远程机器独立页面地址 |
| fleet_add_machine | 注册机器(id / host / user / port / command / cwd / tags) |
| fleet_list_machines | 列出机器、连接状态、agent 信息 |
| fleet_remove_machine | 移除机器及其工作流绑定 |
| fleet_health | 建连 + ACP 握手,报告 agent 与能力 |
| fleet_sessions | 列出可续会话(ACP session/list) |
| fleet_delegate | 派活:立即返回 delegate_id |
| fleet_wait | 等待完成或需要输入(默认 60s,上限 120s) |
| fleet_status | 不等待,直接读快照 |
| fleet_answer | 回答 permission(allow/reject)或 question(text) |
| fleet_cancel | 取消(发 session/cancel) |
路由与升级
会话路由(三层)
- 绑定:
workstream→ 远程 session id(写在state.json里,确定性) - 匹配:同 cwd + 7 天内活跃 + 标题相似度达阈值
- 新建:都不满足
不传 workstream 时,插件会用「同机器 + 同 cwd + 标题相似」去找已有工作流;找不到就新建一个。
返回里的 route_reason 和 candidates 是给你纠偏用的:
{
"mode": "binding",
"route_reason": "workstream is bound to this session",
"candidates": []
}升级策略
默认规则(src/policy.js):
- 只读 → 直接允许
- 工作区内编辑/命令 → 直接允许(
autoEdit: true) - 工作区外 → 升级
- 不可逆 → 一律升级:
rm -rf、git push --force、git reset --hard、删分支、drop/truncate、sudo、curl | sh、deploy、migration、密钥、.env
配置方式(优先级从低到高):
# 环境变量:JSON
export DSH_FLEET_POLICY='{"autoEdit":false}'
export DSH_FLEET_DEFAULT_CWD='/home/luis/work'
export DSH_FLEET_STATE='/var/lib/dsh/fleet.json'
export DSH_FLEET_SSH='/usr/bin/ssh'或在 bundle 行上配 config(profile patch 同 id 覆盖):
- id: remote-fleet
config:
policy:
autoEdit: false
defaultCwd: /home/luis/work状态文件
$DSH_HOME/remote-fleet/state.json(可用 DSH_FLEET_STATE 覆盖),权限 0600:
{
"version": 1,
"machines": {
"pc4090": { "id": "pc4090", "host": "pc4090", "user": "luis", "cwd": "/home/luis/work/app", "command": "dsh --profile acp" }
},
"workstreams": {
"login-refactor": { "machine": "pc4090", "sessionId": "session-1f2e...", "cwd": "/home/luis/work/app", "title": "收尾登录重构", "updatedAt": "2026-10-31T01:20:00.000Z" }
}
}删掉它不会损坏任何远程会话,只会丢掉「工作流 → 会话」的绑定。
目录结构
src/
index.js # cordis 插件入口(name / inject / apply)
tools.js # 10 个模型侧工具定义
fleet.js # 机器注册表、连接池、委托编排
delegate.js # 一次委托的状态机(routing → running → needs_input → done)
router.js # 会话选择:绑定 / 相似度 / 新建
policy.js # 升级规则
capsule.js # 上下文胶囊
store.js # JSON 持久化(原子写)
acp/client.js # ACP v1 客户端(JSON-RPC over stdio)
acp/transport.js # SSH / 本地进程传输
util/ndjson.js # 换行分隔 JSON 编解码
test/
mock-agent.js # 最小 ACP agent(测试用)
*.test.js # 28 个用例测试
npm test覆盖:ACP 握手、prompt 流、permission 往返、会话列表、路由三层、策略分级、委托全流程(新建 / 绑定续接 / 提问 / 审批允许 / 审批拒绝)、插件工具注册形状。
测试用 test/mock-agent.js 当远程 agent,走真实子进程和真实 NDJSON 编解码,只有 SSH 被换成本地进程。
已知限制
- 同一会话没有强制互斥:两个委托同时命中同一个 session 会并发写。v1 加按 session 的队列。
- 提问检测是启发式的:远程以
?/?结尾的消息会被判为needs_input。误判时用fleet_answer回一句即可。 - ACP elicitation 未处理:DSH 的 ACP server 目前把问题当普通消息发出,所以走的是「回合结束 + 问号」这条路。
- 不支持 resume 的 agent 会退化成新会话,并在
recent_events里记一条 warning。 - 客户端 UI 未做:所有操作走对话和工具。
- Codex 未接:需要另一个适配器(
codex exec resume+ 事件流)。
安全
- 插件在 DSH 进程内运行,不受 workspace 沙箱限制;安装即授权。
- SSH 能做的事 = 这台机器能做的事,用专用 key、最小权限用户。
- 默认策略宁可多问:不可逆操作永不自动放行。
state.json只存主机名和工作流绑定,不存密钥。
