# dsh-remote-subagent

[![npm](https://img.shields.io/npm/v/dsh-remote-subagent.svg)](https://www.npmjs.com/package/dsh-remote-subagent)
[![license](https://img.shields.io/npm/l/dsh-remote-subagent.svg)](./LICENSE)

[English](README-en.md) · [简体中文](README.md)

DSH（DeepSeek Harness）远端子代理插件：本地会话用 `remote_agent` 工具把任务**派给远端 DSH**（SSH 上的另一台机器），远端 agent 用它自己的模型、工具与沙箱干活，思考 / 工具调用 / 工具结果 / 最终答复**实时流回**本地会话，并可在同一远端会话上追加消息——用法与内置 subagent 类似，但执行发生在远端机器里。

```
本地会话 ── remote_agent 工具 ──SSH──> 远端 dsh headless --json <task>
   ▲                                        │
   └──── 事件流（thinking / tool_call / tool_result / final）◄──┘
```

## 为什么用远端 `dsh headless --json`

DSH 自带 headless 入口，`--json` 会把一次 run 的过程按行输出为 JSON 事件；`--session-id` 可续跑同一个 Session：

```bash
dsh headless --json '<task>'                     # 新远端会话
dsh headless --json --session-id <id> '<msg>'    # 同一会话续跑
```

插件通过 SSH 的 exec 通道直接读这个流，因此：

- 不需要在远端开公网端口，也不需要 token / WebSocket 协议；
- 远端 agent 用**它自己**的模型配置、工具、沙箱与 session 存储（session 落在远端 `$DSH_HOME/sessions`，在远端 DSH Web UI 里同样可见）；
- 追加消息用 `--session-id`，语义与 subagent 的 send 一致；
- 事件流本身就是「工作过程」，不需要额外埋点。

## 安装

```bash
# 从 npm 安装到当前 profile（bundle 形式）
dsh plugin --profile <profile> add dsh-remote-subagent
# 或在 DSH 插件市场/plugin_manager 里安装 dsh-remote-subagent
```

依赖会自动安装（`ssh2`）。启用后：左侧栏出现「远端代理」图标，模型侧多出 `remote_agent` 工具。

## 机器从哪来

**不重复配置 SSH 清单**：目标机器读取 dsh-remote 的注册表

```
$DSH_HOME/remote-workspaces/machines.json
```

解析顺序：插件 `config.machineId` → 注册表 `currentId` → 第一个已保存机器 → `config.machine`（独立部署无 dsh-remote 时用）。支持密码、私钥（含 passphrase）、SSH agent。主机密钥采用 TOFU：首次连接把指纹记录到 `$DSH_HOME/dsh-remote-subagent/known_hosts.json`，之后必须匹配。

## 工具：`remote_agent`

| action | 作用 | 关键参数 |
|---|---|---|
| `spawn` | 起一次新的远端 run（独立远端会话） | `task`, `machine?`, `cwd?` |
| `wait` | 阻塞等 run 结束或超时（默认 30s，上限 120s；超时不影响远端继续跑） | `runId`, `waitSeconds?` |
| `get` | 状态 + **最近** N 条过程事件 + 结果 | `runId`, `eventLimit?` |
| `send` | 给同一远端会话追加消息（新 run，`--session-id` 续跑，turn+1） | `runId`, `task` |
| `list` | 最近的 run 列表 | `limit?` |
| `cancel` | 终止远端进程（按 pid 文件 `kill -TERM`） | `runId` |
| `machines` | 可用的远端机器 | — |

典型用法：

```
remote_agent({action:"spawn", task:"在 /opt/app 跑一次只读健康检查：磁盘、内存、监听端口，最后总结"})
  → runId
remote_agent({action:"wait", runId, waitSeconds:120})
remote_agent({action:"get", runId, eventLimit:30})
remote_agent({action:"send", runId, task:"把监听端口按大小排序再列一次"})
```

**复用语义**：`spawn` 永远新建远端会话（等于 subagent 的 spawn）；`send` 复用该 run 所属的同一远端会话（等于 subagent 的 send_message）。多个 `spawn` 并发 = 多个互不共享上下文的远端会话；同一会话同一时刻只能跑一个 turn。run 历史与 `sessionId` 持久化在本地，DSH 重启后仍可 `send` 续跑。

## 面板（Client 半）

左侧栏「远端代理」→ 主面板：

- 左列：新建任务（选机器 + 任务文本 + 派活）、run 列表（状态点 / 任务摘要 / 机器 / 用时）；
- 右列：run 详情（状态、远端 session id、cwd、轮次、事件数、token）、**实时事件时间线**（1.5s 轮询增量、自动滚底）、结果框、终止 / 刷新、追加消息输入框。

数据接口（同源；Desktop 走 `/api` 前缀，Web 走裸路径）：

```
GET  /dsh-remote-subagent/runs
GET  /dsh-remote-subagent/run?id=<run>&from=<seq>&limit=<n>
POST /dsh-remote-subagent/action   {action: spawn|send|cancel, ...}
```

## 配置（可选）

插件配置在 profile 的 `cordis.patch.yml` 的 `config:` 下，全部可省略：

| 字段 | 默认 | 说明 |
|---|---|---|
| `machineId` | `''` | 默认机器（id/名称/host） |
| `machinesFile` | `''` | 自定义机器注册表路径 |
| `headlessCommand` | `dsh headless` | 远端命令前缀 |
| `cwd` | `''` | 远端工作目录（空 = 机器 home） |
| `maxConcurrent` | `4` | 并行 run 上限 |
| `timeoutSeconds` | `900` | 单 run 上限，超时自动终止远端进程 |
| `maxEvents` | `4000` | 每个 run 保留的事件上限 |
| `connectTimeoutMs` | `20000` | SSH 连接超时 |

## 本地数据

- `$DSH_HOME/dsh-remote-subagent/runs/<runId>.json`：每个 run 的元数据 + 事件 transcript（重启后历史仍在；重启瞬间仍在跑的 run 标记 failed/interrupted，但保留 session id，可 `send` 续跑）；
- `$DSH_HOME/dsh-remote-subagent/known_hosts.json`：TOFU 主机指纹。

## 实测状态（v0.1.0，2026-11-04）

在一个 Linux 远端（DSH 0.2.0-rc.2，SSH 密码登录）上跑通：

- `spawn` → 11s / 23 条事件 / turn 1，含 `tool_call` 与 `tool_result`；
- `send` 续跑同一 `session-*`，turn 2 正确引用上一轮上下文，回答数值与 turn 1 一致；
- DSH 重启 + 插件热重载后 `send` 仍能续上同一远端会话（turn 3）；
- `cancel` 后远端无残留 `dsh headless` 进程，pid 文件清理；
- 面板槽位注册（`sidebar.panellist` id=`remote-agents`、`main` key=`remote-agents`）与三条面板接口的处理器行为均有单测/实测覆盖。

单测覆盖（离线，无网络）：机器解析顺序与报错、事件流解析/分页/续跑/取消/持久化、面板路由、SSH 引用与 TOFU 指纹、Client 槽位注册。

## 已知边界

- 远端需要 `dsh` 在 SSH 登录 PATH 上，且 headless profile 可用；远端版本过老、没有 `--json` / `--session-id` 时不可用；
- 只支持 POSIX 远端（命令用 `sh -c` 包装）；Windows 远端未实现；
- 不支持 dsh-remote 的 `useSshConfig` 别名机器与 ProxyJump 跳板（请用直连条目）；
- 远端 run 的模型/工具/沙箱完全由远端 profile 决定，本地不能改；
- `wait` 超时不会终止远端 run；运行中不支持插话，需 `wait` 或 `cancel` 后再 `send`；
- 事件流覆盖 headless 单 turn；远端 agent 若把活丢进后台 job，其后续输出不在本 run 流里。

## 回滚

- 卸载：从 profile 移除 `dsh-remote-subagent` bundle；
- 清数据：删除 `$DSH_HOME/dsh-remote-subagent/`（run 历史 + TOFU 指纹）；
- 对远端无持久改动：仅 `/tmp/dsh-remote-subagent-*.pid`（正常结束自动清理）与远端 session 目录。

## License

MIT
