dsh-ssh-vault 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +404 -0
- package/CHANGELOG.zh-CN.md +352 -0
- package/CONTRIBUTING.md +179 -0
- package/CONTRIBUTING.zh-CN.md +167 -0
- package/LICENSE +21 -0
- package/README.md +452 -2
- package/README.zh-CN.md +450 -0
- package/SECURITY.md +42 -0
- package/SECURITY.zh-CN.md +38 -0
- package/assets/banner.png +0 -0
- package/assets/icon-180.png +0 -0
- package/assets/screenshot-1.png +0 -0
- package/client.js +1134 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +11 -0
- package/index.js +1180 -0
- package/lib/background.js +65 -0
- package/lib/i18n.js +589 -0
- package/lib/locales/ssh.js +130 -0
- package/lib/platform.js +68 -0
- package/lib/ssh.js +723 -0
- package/lib/vault-api.js +417 -0
- package/lib/vault.js +397 -0
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +75 -4
- package/scripts/dhcp-reserve-kali.ps1 +81 -0
- package/scripts/make-market-icon.py +63 -0
- package/scripts/set-secret.mjs +127 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# 贡献指南
|
|
2
|
+
|
|
3
|
+
[English](CONTRIBUTING.md) | **简体中文**
|
|
4
|
+
|
|
5
|
+
这个插件会**保管别人的口令**,还要在**别人的机器上执行命令**。所以它的门槛比一般小工具高:
|
|
6
|
+
下面第一节的红线不是风格偏好,破了就是真实泄露或真实事故。
|
|
7
|
+
|
|
8
|
+
## 一、红线(违反即拒绝)
|
|
9
|
+
|
|
10
|
+
### 1. 绝不提交凭据、私钥与真实环境信息
|
|
11
|
+
|
|
12
|
+
不要提交:私钥、口令、API key、`%DSH_HOME%` 下的任何文件、真实内网地址、真实主机名、
|
|
13
|
+
真实账号名、会话日志。
|
|
14
|
+
|
|
15
|
+
需要真实主机的测试**一律走环境变量**,源码里不得出现字面值:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// 正确
|
|
19
|
+
const KALI = { host: env.LIVE_KALI_HOST, user: env.LIVE_KALI_USER, pass: env.LIVE_KALI_PASS }
|
|
20
|
+
|
|
21
|
+
// 错误
|
|
22
|
+
const KALI = { host: '198.51.100.7', user: 'root', pass: '123456' }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
示例、测试数据、文档一律使用**文档专用地址**:`192.0.2.0/24`、`198.51.100.0/24`、`203.0.113.0/24`。
|
|
26
|
+
|
|
27
|
+
按下提交键之前自查:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
git status --short # 确认没有 id_rsa / id_rsa.pub / dist / .env
|
|
31
|
+
git diff --cached # 逐眼过一遍,尤其看有没有 IP、账号、口令
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`.gitignore` 已排除常见项,**但那不是保证**——它挡不住一个叫 `config.local.js` 的新文件。
|
|
35
|
+
|
|
36
|
+
### 2. 口令不得进入命令行、日志或对话
|
|
37
|
+
|
|
38
|
+
这是本项目的核心设计约束。任何改动都必须维持:
|
|
39
|
+
|
|
40
|
+
| 位置 | 要求 |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| 本地 `ssh` 命令行 | 口令只能经 `SSH_ASKPASS` 环境变量传递 |
|
|
43
|
+
| 远端命令行与 `ps` | 口令只能走标准输入 |
|
|
44
|
+
| 会话日志 | 工具参数里不得出现口令,只能出现 `sudo: true` 这类开关 |
|
|
45
|
+
| 磁盘 | 只有 AES-256-GCM 密文;明文只允许存在于内存 |
|
|
46
|
+
|
|
47
|
+
改完执行路径后,必须能通过 `test/live.mjs` 中的「口令不出现在远端进程列表」一项。
|
|
48
|
+
|
|
49
|
+
### 3. 保持零运行时依赖
|
|
50
|
+
|
|
51
|
+
不要引入 npm 依赖。插件只调用系统 OpenSSH 客户端(`ssh` / `scp`)与 Node 内置模块。
|
|
52
|
+
`package.json` 里不应出现 `dependencies`。
|
|
53
|
+
|
|
54
|
+
### 4. 不擅自改动用户的 profile 或保险库
|
|
55
|
+
|
|
56
|
+
运行时只读写自己的保险库目录(`%DSH_HOME%\ssh-vault`)。
|
|
57
|
+
不要写 `%DSH_HOME%\.credentials.yaml`、profile 的 `package.json` 或 `cordis.patch.yml`,
|
|
58
|
+
也不要碰用户的 `~/.ssh`。
|
|
59
|
+
|
|
60
|
+
## 二、开发环境
|
|
61
|
+
|
|
62
|
+
- Node.js >= 20(见 `package.json` 的 `engines`)
|
|
63
|
+
- Windows 10 / 11 且启用 OpenSSH 客户端(`ssh -V` 能输出版本)。**本插件按设计仅支持 Windows**——数据密钥由
|
|
64
|
+
DPAPI 保护、askpass 辅助程序是 Windows `.cmd`;`package.json` 声明了 `os: ["win32"]`,运行期也会在其它平台
|
|
65
|
+
拒绝加载。不要重新引入「非 Windows 上把密钥明文落盘」的代码路径。
|
|
66
|
+
- **不需要 `npm install`**——项目零依赖
|
|
67
|
+
|
|
68
|
+
## 三、测试
|
|
69
|
+
|
|
70
|
+
### 提交前必须跑
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
npm test
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
它依次执行三个**免主机**回归,CI 跑的是同一组:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
node test/diagnose.mjs # shell 翻译、八进制解码、失败归因
|
|
80
|
+
node test/unit.mjs # 参数构造、超时/取消保留输出、wait_for / detach 脚本
|
|
81
|
+
node test/background.mjs # 后台任务注册契约
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 需要真实主机时
|
|
85
|
+
|
|
86
|
+
```powershell
|
|
87
|
+
$env:LIVE_KALI_HOST = '<host>'
|
|
88
|
+
$env:LIVE_KALI_USER = '<user>'
|
|
89
|
+
$env:LIVE_KALI_PASS = '<password>'
|
|
90
|
+
$env:LIVE_WIN_HOST = '<host>'
|
|
91
|
+
$env:LIVE_WIN_USER = '<user>'
|
|
92
|
+
$env:LIVE_WIN_KEY = '<private key path>'
|
|
93
|
+
node test/live.mjs
|
|
94
|
+
node test/selftest.mjs
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 硬性要求
|
|
98
|
+
|
|
99
|
+
- 改动 `lib/ssh.js` 的参数构造或诊断 → **必须**补 `test/unit.mjs` / `test/diagnose.mjs` 用例
|
|
100
|
+
- 改动后台任务 → **必须**补 `test/background.mjs`
|
|
101
|
+
- 新增面向模型的行为 → 至少一条单元用例把它钉住
|
|
102
|
+
- **不许为了让测试通过而放宽断言**。如果新用例要求改旧断言,先在 PR 里说明旧断言为什么是错的
|
|
103
|
+
|
|
104
|
+
## 四、代码风格
|
|
105
|
+
|
|
106
|
+
格式由 [`.editorconfig`](.editorconfig) 约束,另有以下约定:
|
|
107
|
+
|
|
108
|
+
- ESM(`import` / `export`),`"type": "module"`
|
|
109
|
+
- 2 空格缩进、单引号、**行尾不加分号**、行宽约 120
|
|
110
|
+
- 导出的函数写 JSDoc(`@param` / `@returns`);**注释用中文,标识符用英文**
|
|
111
|
+
- 纯函数优先:参数构造(`buildSshArgs`)、脚本生成(`buildWaitScript`、`buildDetachScript`)、
|
|
112
|
+
归因(`diagnose`)都保持无副作用,这样才测得动
|
|
113
|
+
- 错误信息面向使用者:说清**发生了什么**与**怎么恢复**,不要只把底层错误抛出去
|
|
114
|
+
|
|
115
|
+
## 五、提交信息
|
|
116
|
+
|
|
117
|
+
采用 [Conventional Commits](https://www.conventionalcommits.org/zh-hans/):
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
<type>: <简短描述>
|
|
121
|
+
|
|
122
|
+
<可选正文:为什么这么改>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`type` 取 `feat` / `fix` / `docs` / `refactor` / `test` / `chore`。
|
|
126
|
+
标题用英文祈使句,正文中英文皆可。**一次提交只做一件事。**
|
|
127
|
+
|
|
128
|
+
## 六、Pull Request
|
|
129
|
+
|
|
130
|
+
1. 行为变更先开 issue 讨论,尤其是涉及权限、执行路径、存储格式的
|
|
131
|
+
2. 分支名 `feat/...`、`fix/...`、`docs/...`
|
|
132
|
+
3. PR 描述写清三件事:**改了什么、为什么、怎么验证的**
|
|
133
|
+
4. 命中以下任一项时,必须在描述里单独说明:
|
|
134
|
+
- 新增了执行路径(口令会经过哪里)
|
|
135
|
+
- 保险库格式变化(是否需要迁移,旧数据怎么办)
|
|
136
|
+
- 新增工具参数(它会不会进入会话日志)
|
|
137
|
+
5. `npm test` 通过,且 `git diff` 里没有凭据
|
|
138
|
+
|
|
139
|
+
## 七、设计约束(架构层面)
|
|
140
|
+
|
|
141
|
+
动代码前先确认没有破坏这些:
|
|
142
|
+
|
|
143
|
+
- **别名寻址**:模型只见别名,地址与凭据由保险库提供。
|
|
144
|
+
不要新增需要模型传主机名、账号或口令的参数。
|
|
145
|
+
- **中断不等于失明**:任何执行路径在超时或被取消时,都必须回传已经收到的输出。
|
|
146
|
+
- **失败要说清归因**:本地密钥问题、公钥未装机、口令错误、防火墙丢包必须能区分;
|
|
147
|
+
「账号或口令不正确」只能在前几项都排除之后出现。
|
|
148
|
+
- **长任务不占用工具调用**:优先用 `detach`(远端脱离)与 `wait_for`(条件等待),
|
|
149
|
+
不要把 `sleep` 轮询写进工具实现。
|
|
150
|
+
- **会话日志是唯一真相**:不新增自定义会话事件;插件状态要么能从日志重建,
|
|
151
|
+
要么放进保险库或存储服务。
|
|
152
|
+
- **存储格式带版本号**:改动 `vault.json` / `key.json` 结构时必须升版本并能读旧数据。
|
|
153
|
+
|
|
154
|
+
## 八、项目结构
|
|
155
|
+
|
|
156
|
+
| 路径 | 职责 |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `index.js` | 插件入口、四个模型面向工具、配置解析、凭据解析 |
|
|
159
|
+
| `lib/vault.js` | 加密保险库(AES-256-GCM + DPAPI / scrypt) |
|
|
160
|
+
| `lib/ssh.js` | OpenSSH 驱动:参数构造、askpass 注入、失败归因、等待与脱离脚本 |
|
|
161
|
+
| `lib/background.js` | `ctx.jobs` 后台任务适配 |
|
|
162
|
+
| `scripts/set-secret.mjs` | 离线口令录入(不经过 DSH,也不经过模型) |
|
|
163
|
+
| `test/` | 回归;前三个免主机,后三个需要真实目标 |
|
|
164
|
+
|
|
165
|
+
## 九、许可
|
|
166
|
+
|
|
167
|
+
提交贡献即表示同意以 [MIT](LICENSE) 授权。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-ssh-vault contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,453 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# dsh-ssh-vault
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**English** | [简体中文](README.zh-CN.md)
|
|
8
|
+
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](package.json)
|
|
11
|
+
[](https://github.com/deepseek-ai)
|
|
12
|
+
[](package.json)
|
|
13
|
+
[](test)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
SSH for DeepSeek Harness agents: host address, port, account, password and private key live in an encrypted vault, and the conversation only ever uses an alias. The model never sees a password, and a password never reaches a command line or a remote process list.
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
**Windows only.** The vault data key is protected by Windows DPAPI and the `SSH_ASKPASS` helper is a Windows `.cmd` wrapper; the plugin declares `os: ["win32"]` and refuses to start anywhere else rather than storing the key as plaintext.
|
|
22
|
+
|
|
23
|
+
Three capabilities in one plugin:
|
|
24
|
+
|
|
25
|
+
- **Model-facing tools** — `ssh_hosts`, `ssh_run`, `ssh_transfer`, `ssh_probe`, so the agent operates remote hosts by alias.
|
|
26
|
+
- **Settings UI** — add, edit and delete hosts in Settings → SSH Connections, and test reachability. A password entered there travels in an HTTP body, never through the session log.
|
|
27
|
+
- **`/hosts` command** — probe hosts and run commands from the conversation without going through the model.
|
|
28
|
+
|
|
29
|
+
Under the hood it drives the local OpenSSH client (`ssh` / `scp`). It does not implement the SSH protocol itself.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
dsh plugin --profile desktop install dsh-ssh-vault
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
From a local checkout. The argument is a path, so `.` works when you are already inside the plugin directory:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# inside the checkout
|
|
41
|
+
dsh plugin --profile desktop install .
|
|
42
|
+
|
|
43
|
+
# or point at it from elsewhere
|
|
44
|
+
dsh plugin --profile desktop install ../dsh-ssh-vault
|
|
45
|
+
|
|
46
|
+
# or install from a packed tarball
|
|
47
|
+
dsh plugin --profile desktop install ./dsh-ssh-vault-<version>.tgz
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**`--profile` is required.** DSH isolates plugins per profile, and one machine can carry several (`desktop`, `cli`, …). Omitting it falls back to whichever profile is currently default, which is an easy way to install into the wrong place. Confirm the landing spot with:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
dsh plugin --profile desktop list
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Restart DSH after installing.** Host-side tools and routes are loaded at startup.
|
|
57
|
+
|
|
58
|
+
**This package is Windows only.** `package.json` declares `os: ["win32"]`, so `dsh plugin ... install` refuses to install it on Linux or macOS; a checkout loaded on another platform refuses to start for the same reason (see [Known limitations](#platform-and-authentication)).
|
|
59
|
+
|
|
60
|
+
### Requirements
|
|
61
|
+
|
|
62
|
+
| Item | Requirement |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Node | ≥ 20 |
|
|
65
|
+
| Machine | Windows 10 / 11 with the OpenSSH Client enabled (`ssh -V` prints a version) |
|
|
66
|
+
| Platform | **Windows only** (`win32`) — installation and loading are refused on Linux / macOS |
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
1. **Install, then restart DSH.**
|
|
71
|
+
2. Open **Settings → SSH Connections**, click "Add connection", fill in alias, address, port, account, and choose password or private-key authentication.
|
|
72
|
+
Each card carries a health dot that is checked automatically: grey = not checked, yellow = checking, green = reachable, red = unreachable.
|
|
73
|
+
3. Say something in the conversation, for example:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
run uptime on kali
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The model calls `ssh_run` and the result appears as a terminal card.
|
|
80
|
+
|
|
81
|
+
Do not paste passwords into the conversation. A password entered in Settings goes straight into the encrypted vault and never enters the model context.
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### Model-facing tools
|
|
86
|
+
|
|
87
|
+
| Tool | Purpose | Typical phrasing |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `ssh_hosts` | Manage the vault (list / show / add / update / remove) | "which hosts do I have", "change kali's note" |
|
|
90
|
+
| `ssh_run` | Run a command on a host, return stdout and stderr | "check disk usage on kali" |
|
|
91
|
+
| `ssh_transfer` | Upload or download a file (scp) | "copy this file to /tmp on kali" |
|
|
92
|
+
| `ssh_probe` | Reachability self-check returning system facts | "is kali still reachable" |
|
|
93
|
+
|
|
94
|
+
**Where to see the result:** calls to `ssh_run`, `ssh_probe` and `ssh_transfer` render as a **terminal card** — a header with "host · command" and a state dot, above a scrollable monospace output area. The dot is blue while running, grey on success, red on failure (including a non-zero exit code).
|
|
95
|
+
|
|
96
|
+
### Settings UI
|
|
97
|
+
|
|
98
|
+
Settings → SSH Connections provides:
|
|
99
|
+
|
|
100
|
+
- A host list with a health dot per card; hover for detail
|
|
101
|
+
- Add / edit / delete connections (the alias is editable, credentials move with it)
|
|
102
|
+
- A "Test connection" button per card, whose result also drives the dot
|
|
103
|
+
- "Open in file manager" for the vault directory
|
|
104
|
+
- Password and private-key authentication; a key can be uploaded whole and is written into the vault with its permissions tightened
|
|
105
|
+
|
|
106
|
+
### Language
|
|
107
|
+
|
|
108
|
+
Everything the plugin shows follows the language chosen in **Settings → General → Language**; there is no separate switch here.
|
|
109
|
+
|
|
110
|
+
- The settings page and the in-conversation SSH cards get their copy from the host's locale service, so they change the moment the language does.
|
|
111
|
+
- `/hosts` output, transport diagnostics, Settings-API errors, vault errors and background-job status are generated on the Host; they read the same persisted preference (`locale.preference`) and follow later changes, falling back to the request's `Accept-Language` only when nothing was ever chosen.
|
|
112
|
+
- **Model-facing definitions stay English**: tool names, descriptions and parameter schemas belong to the model interface, not to the UI.
|
|
113
|
+
|
|
114
|
+
### The `/hosts` command
|
|
115
|
+
|
|
116
|
+
Start a line with `/` in the composer and the command runs **without going through the model**.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
/hosts list every host, grouped into online / offline
|
|
120
|
+
/hosts kali probe one
|
|
121
|
+
/hosts kali,win10 probe several (either comma form works)
|
|
122
|
+
/hosts kali; apt update run a command on kali
|
|
123
|
+
/hosts kali,win10; uptime run on every reachable host
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- Separators accept both ASCII and full-width forms: `,` and `,`, `;` and `;`
|
|
127
|
+
- If the text after the semicolon reads as natural language (e.g. "check the disk"), the command **does not run it** and tells you to send that sentence to the conversation instead — command output never reaches the model, so such requests cannot go through the command
|
|
128
|
+
- Output renders as **two cards**: reachability and command execution, each with its own state dot
|
|
129
|
+
- **Picking `/hosts` from the `/` menu only claims it**: the composer shows the command token with its hint. Press Enter again to run it bare (list hosts), or type `alias; shell command` as its arguments. To go back to a normal prompt instead, delete the command token — until then, whatever you type is the command's argument
|
|
130
|
+
- **Before the first message the result appears above the composer.** DSH treats a transcript containing only command rows as an inactive view, so a bare `/hosts` in a brand-new conversation used to look like "nothing happened" until you sent a message. The plugin now mirrors the `/hosts` cards (running state included) in the composer dock while the session is still blank; once the first message exists the transcript takes over, so nothing is duplicated
|
|
131
|
+
- A bare call probes only when the health cache is cold, in parallel and with a 4s budget, so it answers within seconds even when several hosts are unreachable
|
|
132
|
+
|
|
133
|
+
## Architecture
|
|
134
|
+
|
|
135
|
+
### Two halves
|
|
136
|
+
|
|
137
|
+
This is a **Host + Client** plugin; the two halves load through different mechanisms:
|
|
138
|
+
|
|
139
|
+
| Half | Entry | Loading |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| **Host** | `index.js` | Cordis plugin, inserted into the Loader through `cordis.patch.yml` |
|
|
142
|
+
| **Client** | `client.js` | Browser module, declared via `dsh.client` in `package.json` and injected into the page by the host |
|
|
143
|
+
|
|
144
|
+
The Host half carries every sensitive operation: vault encryption, driving the local `ssh` client, web routes, and the registration of commands and tools.
|
|
145
|
+
The Client half only renders and collects form input; it never executes a command.
|
|
146
|
+
|
|
147
|
+
### Data flow
|
|
148
|
+
|
|
149
|
+
```mermaid
|
|
150
|
+
flowchart TB
|
|
151
|
+
subgraph entry [Entry points]
|
|
152
|
+
chat[Conversation / model tool call]
|
|
153
|
+
settings[Settings form]
|
|
154
|
+
cmd["/hosts command"]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
subgraph host [Host half — index.js]
|
|
158
|
+
resolve[Resolve alias → read credentials → build ssh args]
|
|
159
|
+
vault[(Vault<br/>AES-256-GCM ciphertext)]
|
|
160
|
+
api["Web routes /ssh-vault/*"]
|
|
161
|
+
exec[Spawn local ssh / scp]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
subgraph client [Client half — client.js]
|
|
165
|
+
toolcard[Tool terminal card]
|
|
166
|
+
cmdcard["/hosts double card"]
|
|
167
|
+
form[Connection form]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
proxy[Remote host]
|
|
171
|
+
|
|
172
|
+
chat --> resolve
|
|
173
|
+
cmd --> resolve
|
|
174
|
+
settings -->|POST JSON| api
|
|
175
|
+
form -->|same-origin, document-relative| api
|
|
176
|
+
api --> vault
|
|
177
|
+
resolve --> vault
|
|
178
|
+
resolve --> exec
|
|
179
|
+
exec -->|stdout / stderr| toolcard
|
|
180
|
+
exec -->|stdout / stderr| cmdcard
|
|
181
|
+
exec <--> proxy
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Three invariants hold across that chain:
|
|
185
|
+
|
|
186
|
+
1. **A password never reaches a command line** — local ssh receives it through `SSH_ASKPASS`; remote sudo is primed over stdin
|
|
187
|
+
2. **Only ciphertext touches disk** — AES-256-GCM, with the data key protected by Windows DPAPI (or, optionally, derived from a master passphrase via scrypt)
|
|
188
|
+
3. **`~/.ssh` is never touched** — `known_hosts` points into the vault directory and `HashKnownHosts` is enabled
|
|
189
|
+
|
|
190
|
+
## Configuration
|
|
191
|
+
|
|
192
|
+
The plugin has **no user-configurable options**. `config: {}` in `cordis.patch.yml` is an empty object, and the Loader entry `id` is fixed as `ssh-vault`.
|
|
193
|
+
|
|
194
|
+
Behaviour is shaped by the following:
|
|
195
|
+
|
|
196
|
+
| Item | Notes |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| Vault directory | Defaults to `$DSH_HOME/ssh-vault`; `DSH_HOME` is decided by DSH |
|
|
199
|
+
| Key protection | `dpapi` (Windows DPAPI, the default) or `passphrase` (scrypt-derived key, no key material on disk). `file` stores the key as plaintext and is an explicit opt-in only |
|
|
200
|
+
| Timeouts | `ssh_run` defaults to 120s, adjustable, capped at 30 minutes; commands inside `/hosts` are fixed at 120s |
|
|
201
|
+
| Health checks | Settings page and `/hosts` probe with a 15s timeout; the background sweep refreshes every 5 minutes |
|
|
202
|
+
|
|
203
|
+
## API and extension points
|
|
204
|
+
|
|
205
|
+
Host-side services are reached through `ctx.inject` as optional dependencies; a missing service does not fail the plugin, it just disables that capability.
|
|
206
|
+
|
|
207
|
+
### Model tools (`ctx.tools.register`)
|
|
208
|
+
|
|
209
|
+
| Tool | Key parameters |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `ssh_hosts` | `action` required (list / show / add / update / remove); `reveal` controls whether address and account are masked |
|
|
212
|
+
| `ssh_run` | `host`, `command` required; optional `sudo`, `timeoutMs`, `workdir`, `run_in_background`, `wait_for`, `bindAddress` |
|
|
213
|
+
| `ssh_transfer` | `host`, `direction`, `local`, `remote` required; optional `recursive` |
|
|
214
|
+
| `ssh_probe` | `host` required; optional `timeoutMs`, `bindAddress` |
|
|
215
|
+
|
|
216
|
+
The three terminal-style tools also register `presentCall` / `presentResult` hooks, but **the current DSH version has no consumer for them**; presentation is handled by the slots below.
|
|
217
|
+
|
|
218
|
+
### Client slots (`ctx.slots.register`)
|
|
219
|
+
|
|
220
|
+
| Slot | Key | Effect |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `settings.section` | `id: ssh-vault`, `order: 20`, `label` | The "SSH Connections" page in the settings panel |
|
|
223
|
+
| `tool.call.toolview` | `ssh_run`, `ssh_probe`, `ssh_transfer` | Renders those tool calls as terminal cards |
|
|
224
|
+
| `conversation.chat.commandview` | `hosts` | Renders `/hosts` output as two terminal cards |
|
|
225
|
+
|
|
226
|
+
The `tool.call.toolview` key space is open; none of these three names were taken, so no built-in UI is shadowed.
|
|
227
|
+
|
|
228
|
+
All three registrations also carry `locale: 'ssh-vault'` — the namespace registered with the host's locale service — which is what makes the rendered cards re-render and re-read their copy when the language changes.
|
|
229
|
+
|
|
230
|
+
### Human command (`ctx.commands.register`)
|
|
231
|
+
|
|
232
|
+
| Command | Notes |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `/hosts` | Probe hosts / run a command on them. Form: `alias[,alias][; shell command]` |
|
|
235
|
+
|
|
236
|
+
The command's `description` and `input.hint` are **re-registered only when the interface language changes**. The disposer returned by `commands.register` destroys the current definition, so replacing it while the user is typing arguments strips the composer of its command context (backspace stops working, and the UI needs a restart). That is why registration is never driven by timers or health results; a locale switch is a deliberate Settings action with nothing being typed, making it the one safe moment to swap the definition.
|
|
237
|
+
|
|
238
|
+
### HTTP routes (`ctx.webServer.register`)
|
|
239
|
+
|
|
240
|
+
Mounted at `/ssh-vault` (`kind: 'prefix'`, **no trailing slash**). `POST` only, JSON in and out, with an envelope of `{ ok: true, value }` or `{ ok: false, error }`.
|
|
241
|
+
|
|
242
|
+
| Path | Effect |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| `/ssh-vault/list` | Host list (address and account masked, no passwords) |
|
|
245
|
+
| `/ssh-vault/save` | Create / update / rename |
|
|
246
|
+
| `/ssh-vault/remove` | Delete |
|
|
247
|
+
| `/ssh-vault/test` | Reachability probe |
|
|
248
|
+
| `/ssh-vault/reveal` | Return unmasked address and account |
|
|
249
|
+
| `/ssh-vault/openVault` | Open the vault directory in the system file manager |
|
|
250
|
+
|
|
251
|
+
`openVault` **accepts no path argument at all** — it can only open the plugin's own vault directory.
|
|
252
|
+
|
|
253
|
+
Every request crosses the Host Connection trust fence **before** the handler looks at the method, the body or the vault: it calls `ctx.connection.requestRejection(req)`, the same Host/Origin check and browser-session authentication used by `/api`. A rejected request gets `401` / `403` and never reaches the vault, so a DNS-rebinding or cross-site call cannot read host data or trigger a probe.
|
|
254
|
+
|
|
255
|
+
### About `ctx.systemPrompt.section`
|
|
256
|
+
|
|
257
|
+
**This plugin does not use that extension point.** Every tool carries its own `description`, and no section is injected into the system prompt, so prompt length and ordering are unaffected.
|
|
258
|
+
|
|
259
|
+
## Lifecycle
|
|
260
|
+
|
|
261
|
+
### Startup
|
|
262
|
+
|
|
263
|
+
1. The Loader reads `cordis.patch.yml` and inserts the entry with `id: ssh-vault`
|
|
264
|
+
2. The host `import`s `index.js` and calls `apply(ctx, config)`
|
|
265
|
+
3. `apply` opens the vault, registers the four tools, and — where the services exist — the web routes, the `/hosts` command and the background health timer. It also subscribes to the host settings document so it can read the interface language and follow later switches
|
|
266
|
+
4. The Client half is injected into the page per the `dsh.client` declaration, and its `apply` registers the three slots and its `ssh-vault` dictionary with the locale service
|
|
267
|
+
|
|
268
|
+
### Unload and resource reclamation
|
|
269
|
+
|
|
270
|
+
Async resources are wrapped in `ctx.effect`, and Cordis runs the returned cleanups in reverse order on teardown:
|
|
271
|
+
|
|
272
|
+
- Tool and command disposers (including the locale-switched command definition)
|
|
273
|
+
- Web route disposers
|
|
274
|
+
- The settings-document subscription and its startup retry timers
|
|
275
|
+
- The health sweep's `setTimeout` / `setInterval`
|
|
276
|
+
- The vault handle
|
|
277
|
+
- The client's locale dictionary and slot registrations
|
|
278
|
+
|
|
279
|
+
Vault data **survives uninstall** and is available again after reinstall.
|
|
280
|
+
|
|
281
|
+
### Hot update
|
|
282
|
+
|
|
283
|
+
The two halves behave differently, and this is easy to get wrong:
|
|
284
|
+
|
|
285
|
+
| Change | Takes effect by |
|
|
286
|
+
| --- | --- |
|
|
287
|
+
| `client.js` and slot rendering | Reinstall the bundle, then **reload the page** |
|
|
288
|
+
| `index.js` / `lib/**` | **Restart DSH**; reinstalling does not replace an already-loaded module generation |
|
|
289
|
+
| `package.json` `version` | The version shown in Settings is read from this file by the Host |
|
|
290
|
+
|
|
291
|
+
**Do not infer that the browser has new code from an unchanged slot id** — slot registration reflects the host side, not the module actually running in the page.
|
|
292
|
+
|
|
293
|
+
## Known limitations
|
|
294
|
+
|
|
295
|
+
### Platform and authentication
|
|
296
|
+
|
|
297
|
+
- **Windows only.** The data key is protected by Windows DPAPI and the `SSH_ASKPASS` helper is a Windows `.cmd`; the repository therefore declares `os: ["win32"]`. There is no Linux/macOS mode: running the code there would have to fall back to a plaintext `key.json`, so the plugin refuses to load instead. SSH still targets Linux/Unix hosts *remotely* — the restriction is on the machine running DSH.
|
|
298
|
+
- **`dpapi` is the default key protection; `passphrase` is available** (a master passphrase, with the key derived through scrypt). `file` stores the key as plaintext and must be chosen explicitly; it is not a fallback.
|
|
299
|
+
- **The Settings UI supports only password and private-key authentication.** The `agent` auth type can be set through `ssh_hosts add`, but the form cannot enter or edit it
|
|
300
|
+
- **Remote shells supported: bash / sh / cmd / powershell.** Sudo priming applies to POSIX shells only
|
|
301
|
+
|
|
302
|
+
### Concurrency
|
|
303
|
+
|
|
304
|
+
- **Connections to one host are serialised in-process.** Every call opens a fresh SSH
|
|
305
|
+
connection, and a burst of them — especially concurrent `scp` — trips sshd's connection
|
|
306
|
+
limits and accumulates failed-auth counters toward an account lock. Background jobs are
|
|
307
|
+
exempt from the queue so a long task does not block that host for everything else.
|
|
308
|
+
- **The vault has no cross-process lock.** Two DSH instances (different profiles pointing at the same vault directory) writing at the same time means the later write wins
|
|
309
|
+
- Within one profile, writes from several sessions serialize on the event loop, but frequent concurrent edits of the same host are still not advisable
|
|
310
|
+
|
|
311
|
+
### Account lockout
|
|
312
|
+
|
|
313
|
+
Windows OpenSSH disconnects a **locked** account outright instead of returning
|
|
314
|
+
`Permission denied`, so a locked account looks like a network problem: the port answers, the
|
|
315
|
+
banner arrives, then the connection is reset. If "port open, banner fine, still cannot
|
|
316
|
+
connect" shows up, check the account before chasing the firewall — `net user <account>` on
|
|
317
|
+
Windows, `faillock --user <account>` on Linux. Locking the management account also kills
|
|
318
|
+
key-based login, so keep a second administrative path available when experimenting with
|
|
319
|
+
lockout policy. `ssh_run` warns before commands that change account state.
|
|
320
|
+
|
|
321
|
+
### Host keys
|
|
322
|
+
|
|
323
|
+
`StrictHostKeyChecking=accept-new` is used: **the first connection is trusted automatically**, and any later fingerprint change is refused. There is no separate fingerprint-verification flow, so judging whether a first-time target is trustworthy is up to the operator.
|
|
324
|
+
|
|
325
|
+
### Output and timeouts
|
|
326
|
+
|
|
327
|
+
- `ssh_run` caps a single result at 200000 characters and truncates beyond that
|
|
328
|
+
- Each command inside `/hosts` echoes at most 40 lines
|
|
329
|
+
- Commands inside `/hosts` have a fixed 120s timeout that is not configurable
|
|
330
|
+
- Foreground tool calls have **no streaming output**: the result appears in one piece when the command ends. To watch a long job, use `run_in_background`, whose output lands in a job card
|
|
331
|
+
|
|
332
|
+
### Interface
|
|
333
|
+
|
|
334
|
+
- The **navigation icon next to "SSH Connections" is drawn by the host from the section id**. The `settings.section` registration accepts only `id` / `order` / `label`, so a plugin cannot supply its own icon
|
|
335
|
+
- Command arguments have **no candidate-list mechanism** (`CommandInputDescriptor` carries only `hint` and `attachments`), so `/hosts` cannot offer alias completion
|
|
336
|
+
- A command with an `input` hint is **claimed, not executed**, when picked from the `/` menu: this is DSH behaviour and the plugin cannot bypass it. The hint (localized) explains that a second Enter runs the command bare
|
|
337
|
+
- **DSH does not treat a transcript made only of command rows as an active view.** `/hosts` compensates with the composer-dock mirror described above; the built-in rule still applies to other plugins' commands
|
|
338
|
+
- The **interface language is read from the shared host settings**, so with several browsers attached to one host the most recently written preference wins; DSH Desktop is a single-user surface, where this is the intended behaviour
|
|
339
|
+
|
|
340
|
+
### Open questions
|
|
341
|
+
|
|
342
|
+
Not verified in detail; check the source or upstream docs before relying on them:
|
|
343
|
+
|
|
344
|
+
- Whether DSH exposes a structured field for command results, which would replace the textual section markers (`─── 连接状态:ok ───`) used today
|
|
345
|
+
- Whether the host will connect a consumer to `presentCall` / `presentResult` in a future version
|
|
346
|
+
|
|
347
|
+
## Security boundary
|
|
348
|
+
|
|
349
|
+
### What it touches
|
|
350
|
+
|
|
351
|
+
| Category | Accessed | Detail |
|
|
352
|
+
| --- | --- | --- |
|
|
353
|
+
| Filesystem | **Yes** | The vault directory (`vault.json`, `key.json`, `known_hosts`, uploaded key copies) and any private-key path you supply; **never** `~/.ssh` |
|
|
354
|
+
| Network | **Yes** | Outbound SSH through the local `ssh` / `scp` processes; loopback HTTP routes (same origin, behind the Host Connection trust fence) |
|
|
355
|
+
| Terminal | **Yes** | Spawns the local OpenSSH client; `openVault` launches the system file manager |
|
|
356
|
+
| External APIs | **No** | No third-party service is contacted |
|
|
357
|
+
| User data | **Yes** | Host address, port, account, password, private key, note, tags |
|
|
358
|
+
|
|
359
|
+
### Password handling
|
|
360
|
+
|
|
361
|
+
- **Local SSH**: passed through `SSH_ASKPASS`, never on the command line or in a process list
|
|
362
|
+
- **Remote sudo**: primed over stdin, never on the remote command line or in `ps`
|
|
363
|
+
- **On disk**: only AES-256-GCM ciphertext; the data key is protected by DPAPI or a scrypt-derived key
|
|
364
|
+
- **Model context**: a password never appears in tool arguments or results; one entered in Settings travels in an HTTP body and stays out of the session log
|
|
365
|
+
- **`credentialRef`**: when a host points at an entry in the Harness credential store, the password is read at connect time and never passes through this plugin
|
|
366
|
+
|
|
367
|
+
### Trust boundary
|
|
368
|
+
|
|
369
|
+
The web routes ride on the DSH web server and ask the Host Connection service for a rejection first (the same Host/Origin fence and browser-session authentication as `/api`). The plugin adds no authentication of its own beyond that, so **do not expose the DSH web port to an untrusted network**.
|
|
370
|
+
|
|
371
|
+
### Reporting a vulnerability
|
|
372
|
+
|
|
373
|
+
See [SECURITY.md](SECURITY.md).
|
|
374
|
+
|
|
375
|
+
## FAQ
|
|
376
|
+
|
|
377
|
+
**"SSH Connections" is missing from Settings after installing.**
|
|
378
|
+
Check you installed into the right profile: `dsh plugin --profile <name> list`. Then restart DSH — the client bundle is injected at page bootstrap, so a page reload alone is not enough.
|
|
379
|
+
|
|
380
|
+
**A tool call reports a missing route, or Settings says the host is still running old code.**
|
|
381
|
+
The host code was updated but the process was not restarted. Restart DSH.
|
|
382
|
+
|
|
383
|
+
**The health dot stays red although the host answers ping.**
|
|
384
|
+
The dot tests the **SSH port 22**, not ICMP. Check that `sshd` is running, that a firewall allows the port, and that the address stored in the vault is still current.
|
|
385
|
+
|
|
386
|
+
**A connection test reports "connection not established within 15000ms".**
|
|
387
|
+
Again a network-layer signal: the address changed, a firewall is blocking, or the machine is up without `sshd`. It is unrelated to login credentials.
|
|
388
|
+
|
|
389
|
+
**A word gets a red squiggle while typing `/hosts`.**
|
|
390
|
+
That is why the command is named `hosts`: the composer is a Lexical editor whose plain text nodes are subject to browser spellcheck, and `hosts` is a dictionary word while `ssh` is not.
|
|
391
|
+
|
|
392
|
+
**`/hosts kali,win10` says "not found".**
|
|
393
|
+
Check the alias spelling against the vault; it is case-sensitive. Run `/hosts` with no arguments to list the valid aliases.
|
|
394
|
+
|
|
395
|
+
**A private key upload is rejected.**
|
|
396
|
+
Three common causes: a `.pub` public key was chosen, a PuTTY `.ppk` was chosen (convert it with `puttygen` first), or the file has no `BEGIN … PRIVATE KEY` header.
|
|
397
|
+
|
|
398
|
+
**Output from several hosts runs together.**
|
|
399
|
+
Use the `/hosts host; command` form; the output splits into a reachability card and a command card.
|
|
400
|
+
|
|
401
|
+
**I switched DSH to English but some plugin text is still Chinese.**
|
|
402
|
+
From 1.15.0 the whole surface follows the DSH language. After upgrading, reload the page once (client half) and restart DSH once (host half) to load the new code; after that, switching the language in Settings applies immediately, no reload needed. Model-facing tool definitions stay English by design.
|
|
403
|
+
|
|
404
|
+
**Does it work on Linux or macOS?**
|
|
405
|
+
No. The data key is protected by Windows DPAPI and the `SSH_ASKPASS` helper is a Windows `.cmd`; any non-Windows path would have to store the key as plaintext, so the plugin refuses to load there. `package.json` declares `os: ["win32"]` and installation is rejected elsewhere. The *remote* hosts can of course be Linux/Unix — only the machine running DSH must be Windows.
|
|
406
|
+
|
|
407
|
+
**Client code changed but the interface did not.**
|
|
408
|
+
Reload the page. Host-side changes need a DSH restart.
|
|
409
|
+
|
|
410
|
+
## Contributing
|
|
411
|
+
|
|
412
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The hard rules: **zero runtime dependencies** (Node built-ins only on the Host side; only the host-provided `react` on the Client side), no real credentials or addresses in the repo, and a password must never reach a command line or the session log.
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
npm test # host-free regression suite
|
|
416
|
+
npm run test:live # end-to-end cases against real hosts
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
[MIT](LICENSE)
|
|
422
|
+
|
|
423
|
+
## Cheat sheet: start · use · stop · uninstall
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
# Install (profile required)
|
|
427
|
+
dsh plugin --profile desktop install dsh-ssh-vault
|
|
428
|
+
dsh plugin --profile desktop install . # from inside a local checkout
|
|
429
|
+
|
|
430
|
+
# Confirm where it landed
|
|
431
|
+
dsh plugin --profile desktop list
|
|
432
|
+
|
|
433
|
+
# Reinstall to refresh the installed copy
|
|
434
|
+
dsh plugin --profile desktop remove dsh-ssh-vault
|
|
435
|
+
dsh plugin --profile desktop install dsh-ssh-vault
|
|
436
|
+
|
|
437
|
+
# Uninstall
|
|
438
|
+
dsh plugin --profile desktop remove dsh-ssh-vault
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Starting and stopping** are handled by the DSH process itself: the plugin loads when DSH starts and unloads when DSH exits. It has no separate process and no resident background service.
|
|
442
|
+
|
|
443
|
+
Uninstalling **does not delete the vault**. Data stays in `$DSH_HOME/ssh-vault` and is usable again after reinstall; delete that directory by hand to remove it entirely.
|
|
444
|
+
|
|
445
|
+
Everyday reference:
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
Settings → SSH Connections manage hosts, test reachability, open the vault directory
|
|
449
|
+
/hosts list hosts and their online state
|
|
450
|
+
/hosts kali probe one host
|
|
451
|
+
/hosts kali,win10; uptime run a command on several
|
|
452
|
+
(just talk) let the model pick hosts and call the tools
|
|
453
|
+
```
|