@hunan36/dsh-ssh-helper 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +330 -2
- package/README.zh.md +331 -0
- package/client.js +2786 -0
- package/cordis.patch.yml +13 -0
- package/docs/screenshot-accounts.png +0 -0
- package/docs/screenshot-activity.png +0 -0
- package/docs/screenshot-panel.png +0 -0
- package/docs/screenshot-resource-detail.png +0 -0
- package/host/accounts.js +688 -0
- package/host/api.js +440 -0
- package/host/atomic.js +160 -0
- package/host/discover.js +440 -0
- package/host/gate.js +264 -0
- package/host/history.js +276 -0
- package/host/prompt.js +103 -0
- package/host/resources.js +604 -0
- package/host/shell.js +194 -0
- package/host/ssh.js +460 -0
- package/host/tools.js +1086 -0
- package/host/update.js +992 -0
- package/host/upload.js +399 -0
- package/icon.svg +19 -0
- package/index.js +141 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +75 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-ssh-helper 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,331 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SSH Helper for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@hunan36/dsh-ssh-helper)
|
|
4
|
+
[](https://www.npmjs.com/package/@hunan36/dsh-ssh-helper)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[]()
|
|
7
|
+
[](https://github.com/hunan36/dsh-ssh-helper)
|
|
8
|
+
|
|
9
|
+
SSH accounts, a deployment-directory catalogue, and **approval-gated** remote updates and rollbacks for DeepSeek Harness.
|
|
10
|
+
|
|
11
|
+
`@hunan36/dsh-ssh-helper` gives the agent a small, boring, auditable set of SSH operations: list accounts, look around a server read-only, and — only after you click approve — push a file, run an update, or restore a backup. There is no generic remote-exec tool in the package, and no "update now" button in the panel. Every write crosses DSH's native approval prompt, every time.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
*The Deployment resources view: every resource carries its server, app directory, update command, restart/verify command and backup policy.*
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src="docs/screenshot-accounts.png" width="49%" alt="Accounts view with connection state and legacy import banner">
|
|
19
|
+
<img src="docs/screenshot-activity.png" width="49%" alt="Activity timeline of applies, uploads and rollbacks">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="docs/screenshot-resource-detail.png" width="72%" alt="Resource detail sheet showing the exact commands that the approval prompt will show">
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<em>Left to right, top to bottom: accounts, the update-history timeline, and a resource detail sheet — the panel only displays and copies; it never executes a remote write.</em>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why this plugin
|
|
33
|
+
|
|
34
|
+
An agent that can SSH into production is useful right up until it runs the wrong command unattended. Most SSH plugins solve that either by refusing to write at all, or by writing freely. This one takes the middle path: the agent can discover and catalogue deployment directories on its own, can prepare a full update plan on its own, but the moment a byte is written to a remote host, DSH stops and asks you — showing the exact target, the exact commands and the exact backup path in the prompt.
|
|
35
|
+
|
|
36
|
+
The approval is not a convention the model is politely asked to follow. It is a `tools/pre-execute` handler in the host process that the tool implementation cannot bypass, because the tool bodies never receive an "approved" flag at all.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
| Area | What you get |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Account manager | Add, edit, delete and test SSH accounts. Password auth, or private key with an optional passphrase. |
|
|
43
|
+
| Connection test | Per-account and "test all", from the panel or via `ssh_helper_accounts` with `action="test"` (runs `uname -srm` plus `id -un`). |
|
|
44
|
+
| Legacy import | One-click import of accounts from an existing `@local/dsh-ssh-manager` install, de-duplicated by `host:port:username`. |
|
|
45
|
+
| Read-only discovery | `ssh_helper_discover` scans common roots (`/opt`, `/srv`, `/var/www`, `/data`, `/app`, `/home`) for project markers, then inspects one directory: top-level entries, markers, git state, listening services. File contents are never read. |
|
|
46
|
+
| Resource catalogue | Each deployment resource stores server, app directory, update/restart/verify commands, backup policy, timeout and the facts discovered about it. No credentials. |
|
|
47
|
+
| Update planning | `ssh_helper_update_plan` runs a read-only preflight (directory exists, writable, free space for the backup, markers, git state) and returns a plan id, warnings and blockers. Nothing is modified. |
|
|
48
|
+
| Gated apply | `ssh_helper_update_apply` backs up, runs update → restart → verify, prunes old backups and records history. Always asks first. |
|
|
49
|
+
| Gated rollback | `ssh_helper_update_rollback` moves the current directory aside, extracts the recorded backup, then optionally restarts and verifies. Always asks first. |
|
|
50
|
+
| Gated upload | `ssh_helper_upload` pushes one local file over SFTP, then verifies byte size and SHA-256 **on the server**. Partial verification is reported as `partial`, not `success`. |
|
|
51
|
+
| Update history | Every apply, rollback and upload records its steps, exit codes, durations and output tails (last 200 entries). |
|
|
52
|
+
| System prompt | A `## SSH Helper` section is rebuilt on every turn with the current accounts and resources, and states that the three write tools always require approval. |
|
|
53
|
+
| Bilingual panel | Left-sidebar panel with English and Chinese strings; Accounts / Deployments / Activity views, search, status filters and copy-to-session prompt buttons. |
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
**Requirements**
|
|
58
|
+
|
|
59
|
+
- DSH with the `tools`, `systemPrompt`, `webServer` and `connection` services. Verified against DSH `0.2.2-rc.1`; any build that provides those four host services should work.
|
|
60
|
+
- Node `^22.19.0 || >=24.0.0`. The only runtime dependency is `ssh2`, which works without a compiler: its native `cpu-features` helper is optional and installation succeeds when it cannot be built.
|
|
61
|
+
|
|
62
|
+
### From npm (recommended)
|
|
63
|
+
|
|
64
|
+
Install the package into your DSH profile and add it to the profile's bundle list:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# inside the DSH profile directory
|
|
68
|
+
pnpm add @hunan36/dsh-ssh-helper
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then add `@hunan36/dsh-ssh-helper` to the bundle list of that profile. The package ships its own bundle patch (`cordis.patch.yml`), which mounts the host half as the loader row `ssh-helper`:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
- insert:
|
|
75
|
+
- id: ssh-helper
|
|
76
|
+
name: dsh-ssh-helper
|
|
77
|
+
config: {}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### From the plugin manager (GUI)
|
|
81
|
+
|
|
82
|
+
Sidebar → Plugins → add a plugin, and give it either the npm spec `@hunan36/dsh-ssh-helper` or, for a local checkout, the absolute path of this directory.
|
|
83
|
+
|
|
84
|
+
### From source
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/hunan36/dsh-ssh-helper.git
|
|
88
|
+
cd dsh-ssh-helper
|
|
89
|
+
npm install --ignore-scripts
|
|
90
|
+
npm test
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Point the plugin manager at the checkout directory.
|
|
94
|
+
|
|
95
|
+
### After installing
|
|
96
|
+
|
|
97
|
+
- Host-side changes (tools, API, prompt section, approval gate) need a **DSH restart**.
|
|
98
|
+
- Client-side changes need a **page refresh**.
|
|
99
|
+
- An **SSH Helper** entry appears in the left sidebar. It is not under DSH Settings.
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
Five minutes from zero to a gated update.
|
|
104
|
+
|
|
105
|
+
1. **Add a server.** Sidebar → SSH Helper → Accounts → Add server. Enter host, port, username, and either a password or a private key (plus passphrase). Save, then press **Test** — a success shows the remote `uname` and user.
|
|
106
|
+
|
|
107
|
+
2. **Ask the agent to look around.** Start a session and say:
|
|
108
|
+
|
|
109
|
+
> Scan 10.0.1.24 for deployment directories, find web-api, and save its directory, git state and service unit as a deployment resource.
|
|
110
|
+
|
|
111
|
+
The agent calls `ssh_helper_discover` (`action="scan"`), then `action="inspect"` on the candidate, then `ssh_helper_resources` (`action="save"`). Both discovery actions are read-only.
|
|
112
|
+
|
|
113
|
+
3. **Ask for a plan.**
|
|
114
|
+
|
|
115
|
+
> Give me an update plan for web-api. Don't run anything yet.
|
|
116
|
+
|
|
117
|
+
The agent calls `ssh_helper_update_plan`. You get the plan id, the resolved app directory, the backup archive path, the update/restart/verify commands, the preflight facts, and any warnings or blockers. A plan with blockers cannot be applied.
|
|
118
|
+
|
|
119
|
+
4. **Apply it.**
|
|
120
|
+
|
|
121
|
+
> Go ahead and apply that plan.
|
|
122
|
+
|
|
123
|
+
The agent calls `ssh_helper_update_apply` with the plan id. **DSH shows an approval prompt** listing the resource, server, directory, backup path and the exact commands. Nothing happens until you approve.
|
|
124
|
+
|
|
125
|
+
5. **Watch and verify.** The run appears in the Activity view: one row per step (preflight, backup, update, restart, verify, prune) with exit code, duration and output tails. If it went wrong, say:
|
|
126
|
+
|
|
127
|
+
> Roll back web-api to the last backup.
|
|
128
|
+
|
|
129
|
+
which is again an approval-gated `ssh_helper_update_rollback`.
|
|
130
|
+
|
|
131
|
+
### Optional: upload a build artifact first
|
|
132
|
+
|
|
133
|
+
> Upload /Users/me/build/web-dist.tgz to /tmp/web-dist.tgz on 6.25, then update web-fe.
|
|
134
|
+
|
|
135
|
+
The agent calls `ssh_helper_upload`, you approve the transfer, the file goes over SFTP, and the plugin verifies size and SHA-256 on the server before reporting success. The remote file can then be consumed by the update command, for example:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
test -f /tmp/web-dist.tgz && tar -xzf /tmp/web-dist.tgz -C .
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## The approval model
|
|
142
|
+
|
|
143
|
+
This is the part worth reading before you point the plugin at a real server.
|
|
144
|
+
|
|
145
|
+
### One gate, three tools
|
|
146
|
+
|
|
147
|
+
The plugin registers a single `tools/pre-execute` handler. It inspects the tool name and returns one of two outcomes:
|
|
148
|
+
|
|
149
|
+
- `{ kind: 'ask', reason, displayReason: { en, zh } }` for a **resolvable** write request;
|
|
150
|
+
- `next()` — fall through — for everything else.
|
|
151
|
+
|
|
152
|
+
DSH then requests approval and **only dispatches the tool on an `allowed-once` decision**. A denied, cancelled or unavailable approval channel dispatches nothing. The tool body has no idea whether an approval happened; there is no `confirm`, `force` or `skip_approval` parameter anywhere in the package.
|
|
153
|
+
|
|
154
|
+
### A remote write happens only when all of these are true
|
|
155
|
+
|
|
156
|
+
1. The tool is exactly one of `ssh_helper_upload`, `ssh_helper_update_apply` or `ssh_helper_update_rollback`.
|
|
157
|
+
2. The arguments resolve into a concrete target:
|
|
158
|
+
- **apply** — the plan id exists, has not expired, does not carry blockers, and belongs to the calling agent;
|
|
159
|
+
- **rollback** — a backup archive from a previous `apply` entry resolves for that resource (optionally a specific `history_id`);
|
|
160
|
+
- **upload** — the account resolves, the local path is an existing non-empty regular file under 2 GiB, and the remote path passes the path validator.
|
|
161
|
+
3. You answer the prompt with an allow-once decision for that exact call.
|
|
162
|
+
4. For apply and rollback, no other apply/rollback is already running against that server.
|
|
163
|
+
|
|
164
|
+
### The gate fails closed
|
|
165
|
+
|
|
166
|
+
If the gate cannot be registered at all — a harness that renames `tools/pre-execute`, a plugin-ordering failure, any throw — the host logs a warning and **registers only the read-only tools**. `apply`, `rollback` and `upload` are never mounted. This matters because DSH's `tools/pre-execute` waterfall defaults to *allow*: a write tool without a gate is a write with no approval, so a missing gate must remove the tool rather than leave it unguarded. You get four tools instead of seven, and the agent is told the write tools are unavailable.
|
|
167
|
+
|
|
168
|
+
### The approved target is pinned, not re-resolved
|
|
169
|
+
|
|
170
|
+
For rollback, the archive that the prompt names is the archive that gets extracted. The gate pins the resolved backup entry; the tool body consumes that pin instead of resolving the backup a second time, and the pin is dropped if the resource's app directory, server or history entry changed in the meantime. Without this, an `apply` landing while the prompt is open could silently swap which tree you restore. Apply already had the equivalent guarantee through its `plan_id`.
|
|
171
|
+
|
|
172
|
+
### Commands are fenced in the prompt
|
|
173
|
+
|
|
174
|
+
A resource's update command is a shell script and may legitimately span several lines, so it is not flattened. It is instead wrapped in an explicit `----- command (verbatim) -----` fence, and every other interpolated field (resource name, server, directory, backup path, restart/verify command) is forced onto a single line with control characters and bidirectional overrides removed. A resource name cannot therefore forge a `Backup: disabled.` line in the prompt you read, and a path cannot render reversed.
|
|
175
|
+
|
|
176
|
+
### When the gate falls through
|
|
177
|
+
|
|
178
|
+
If a request does not resolve, **no approval prompt is shown and the tool body returns a guidance message instead**. That is deliberate: a prompt that cannot correspond to a real action is just noise, and an invalid request must never look approved. Cases that fall through:
|
|
179
|
+
|
|
180
|
+
- apply with a missing, unknown, expired or foreign-agent plan id;
|
|
181
|
+
- apply with a plan that has blockers (missing directory, not writable, empty update command, backup dir inside the app dir, not enough free space);
|
|
182
|
+
- rollback with no recorded backup;
|
|
183
|
+
- upload with a missing account, non-absolute or unreadable local path, empty file, oversized file, or a resource belonging to a different server.
|
|
184
|
+
|
|
185
|
+
### Approval presets
|
|
186
|
+
|
|
187
|
+
Approval is enforced by DSH, so the session's approval policy applies. A preset whose approval mode is `never` declines the prompt automatically — the update simply does not run, and the agent gets a refusal. If you want gated updates to work, use a preset that can ask.
|
|
188
|
+
|
|
189
|
+
### What the prompt contains
|
|
190
|
+
|
|
191
|
+
| Tool | Shown in the prompt |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| apply | Plan id, resource name, server, app directory, backup archive path (or `disabled`), update command (truncated at 300 characters), restart and verify commands when set, plan warnings. |
|
|
194
|
+
| rollback | Resource name, server, app directory, backup archive to restore, a note that the current directory is moved aside first, the restore command, restart and verify commands when set, path warnings. |
|
|
195
|
+
| upload | File name, human-readable size, target server, remote path, local path, the associated deployment resource when given, a note that size and SHA-256 are verified after transfer, path warnings. |
|
|
196
|
+
|
|
197
|
+
### What the panel can and cannot do
|
|
198
|
+
|
|
199
|
+
The panel is a read/diagnostics console. It talks to the plugin's own HTTP API (`accounts/*`, `resources/*`, `history/*`) and has **no update or rollback button**. The only way to change a remote host is an approved agent tool call.
|
|
200
|
+
|
|
201
|
+
The API itself is `POST /ssh-helper/api/<method>` with a JSON envelope (`{ ok: true, data }` or `{ ok: false, error: { code, message } }`). Every request is fenced by DSH's `connection.requestRejection`, request bodies are capped at 1 MiB, and no method returns a password, private key or passphrase.
|
|
202
|
+
|
|
203
|
+
## Tool reference
|
|
204
|
+
|
|
205
|
+
Seven tools, all prefixed `ssh_helper_`. Accounts and resources are addressed by id or name (exact name, case-insensitive, then a unique name prefix).
|
|
206
|
+
|
|
207
|
+
| Tool | `action` / key inputs | Writes remotely | Approval |
|
|
208
|
+
| --- | --- | --- | --- |
|
|
209
|
+
| `ssh_helper_accounts` | `action: list \| test`; `account`; `timeout_seconds` (1–120, default 20) | no | not needed |
|
|
210
|
+
| `ssh_helper_discover` | `action: scan \| inspect`; `server`; `roots`; `path`; `max_depth` (1–4, default 3); `limit` (1–200, default 80); `timeout_seconds` (1–600, default 60) | no | not needed |
|
|
211
|
+
| `ssh_helper_resources` | `action: list \| get \| history \| save \| delete`; `resource`; `server`; `name`; `app_dir`; `update_command`; `restart_command`; `verify_command`; `backup_enabled`; `backup_dir`; `backup_keep` (1–50, default 5); `timeout_seconds` (30–3600, default 600); `note` (≤500); `tags`; `discovered` | no (metadata only) | not needed |
|
|
212
|
+
| `ssh_helper_update_plan` | `resource`; `timeout_seconds` (1–600, default 60) | no (read-only preflight) | not needed |
|
|
213
|
+
| `ssh_helper_update_apply` | `plan_id` | yes | **every call** |
|
|
214
|
+
| `ssh_helper_update_rollback` | `resource`; `history_id` (optional) | yes | **every call** |
|
|
215
|
+
| `ssh_helper_upload` | `server`; `local_path`; `remote_path`; `resource` (optional); `timeout_seconds` (5–3600, default 300) | yes | **every call** |
|
|
216
|
+
|
|
217
|
+
There is deliberately **no generic remote-exec tool**. Ad-hoc commands stay in whatever other SSH plugin you already use; the system prompt tells the agent that changes to a catalogued deployment directory must go through `ssh_helper_update_*`, never through a generic exec.
|
|
218
|
+
|
|
219
|
+
Plans are single-use and short-lived: a plan expires after 15 minutes, is bound to the agent that created it, and is dropped after its apply attempt, so the same `plan_id` cannot be replayed.
|
|
220
|
+
|
|
221
|
+
## Security model
|
|
222
|
+
|
|
223
|
+
- **Credentials stay host-side.** `accounts.json` is read and written only by the host process. Every public projection (tool results, HTTP API responses, system prompt, approval reasons, update history) drops the password, private key and passphrase; the API has no endpoint that returns them. The browser keeps secrets only in the open dialog form, and `localStorage` holds exactly one unrelated flag ("legacy import dismissed").
|
|
224
|
+
- **File permissions.** Data files are written to a `.tmp` sibling and renamed into place, with mode `0600` applied to both the temporary file and the final file (best effort — a filesystem that rejects `chmod` cannot be corrected by the plugin). A file that fails to parse is quarantined as `<name>.corrupt-<timestamp>` and the plugin starts from an empty store instead of crashing.
|
|
225
|
+
- **Path validation.** Every remote directory is checked: absolute path, ≤1024 characters, no control characters, no `..` segment, not exactly one of the blocked system directories (`/`, `/bin`, `/boot`, `/dev`, `/etc`, `/lib`, `/lib64`, `/proc`, `/run`, `/sbin`, `/sys`, `/usr`, `/var`, `/var/lib`, `/var/log`, `/var/run`, `/home`), and not anywhere below `/bin`, `/boot`, `/dev`, `/etc`, `/lib`, `/lib64`, `/proc`, `/run`, `/sbin`, `/sys` or `/usr`. The backup directory is validated by the same rules, and a backup directory inside the app directory is rejected.
|
|
226
|
+
- **Everything is quoted.** Every user-controlled value that reaches a remote shell goes through POSIX single-quote quoting (`'…'`, with embedded quotes escaped as `'\''`).
|
|
227
|
+
- **Backup before change.** The default backup is a `tar.gz` of the app directory written to `<parent of appDir>/.ssh-helper-backups/<resource>/<resource>-<UTC stamp>.tar.gz`. If free space on the target filesystem is below `1.2 × directory size + 10 MiB`, the plan carries a blocker and cannot be applied.
|
|
228
|
+
- **No automatic rollback.** If the update command, restart or verify fails, the run stops, keeps the backup and records the failure. Nothing is rolled back behind your back; you decide.
|
|
229
|
+
- **Rollback is reversible.** The current directory is moved to `<appDir>.ssh-helper-before-rollback-<UTC stamp>` before the archive is extracted, so the pre-rollback state survives.
|
|
230
|
+
- **Host keys are accepted as presented (v1).** The client sets `hostVerifier: () => true`. There is no `known_hosts` pinning and no TOFU prompt yet; treat the network path to your servers accordingly, and prefer a VPN or bastion.
|
|
231
|
+
- **Shell is not interactive.** Update, restart and verify commands run as `cd <appDir> && bash -lc <command>` over a non-PTY session, with the SSH user's privileges. Interactive `sudo` prompts cannot be answered, so use `NOPASSWD` sudoers entries or `sudo -n`.
|
|
232
|
+
- **Commands are visible by design.** The update, restart and verify commands appear verbatim in the approval prompt and in the history file. Never inline tokens, passwords or keys into a command; reference a file or an environment variable instead. Captured **output** is a different case: stdout and stderr tails are scrubbed before they are persisted, masking `password` / `passwd` / `passphrase` / `token` / `secret` / `api_key` assignments and PEM private-key blocks. That scrub is defence in depth, not a licence to echo secrets — a secret on its own line, with no `name=value` shape, is not caught.
|
|
233
|
+
- **Local paths are not echoed back.** A failed local-file check returns a path-free message, so the agent cannot use upload errors to probe whether a file exists on your machine or how large it is. The path still appears in the approval prompt, where you are the reader.
|
|
234
|
+
- **Timeouts are local.** When an SSH operation times out, the plugin closes the local connection — the remote process may keep running. Size `timeout_seconds` for long builds and migrations.
|
|
235
|
+
- **Remote output is bounded.** Stdout is capped at 512 KiB and stderr at 256 KiB per command; history keeps the last 8192 characters of each stream.
|
|
236
|
+
|
|
237
|
+
## Data and files
|
|
238
|
+
|
|
239
|
+
Resolution order, first match wins:
|
|
240
|
+
|
|
241
|
+
1. `DSH_SSH_HELPER_DIR` — explicit override (tests, throwaway profiles);
|
|
242
|
+
2. `$DSH_HOME/ssh-helper`;
|
|
243
|
+
3. `~/.dsh/ssh-helper`.
|
|
244
|
+
|
|
245
|
+
| File | Contents | Contains secrets |
|
|
246
|
+
| --- | --- | --- |
|
|
247
|
+
| `accounts.json` | SSH accounts: name, host, port, username, auth type, credential, note, timestamps | **yes** (mode `0600`) |
|
|
248
|
+
| `resources.json` | Deployment resources: server reference, app directory, commands, backup policy, timeout, tags, discovered facts | no (mode `0600`) |
|
|
249
|
+
| `history.json` | Up to 200 apply / rollback / upload entries with per-step commands, exit codes, durations and output tails | no (mode `0600`) |
|
|
250
|
+
|
|
251
|
+
Legacy import reads, in order, `$DSH_HOME/ssh-manager/servers.json` and `$DSH_PROFILE_DIR/data/ssh-manager/servers.json`. Import is manual — the plugin never imports on its own — and entries that fail validation or duplicate an existing `host:port:username` are skipped.
|
|
252
|
+
|
|
253
|
+
## Configuration and limits
|
|
254
|
+
|
|
255
|
+
### Environment variables
|
|
256
|
+
|
|
257
|
+
| Variable | Purpose |
|
|
258
|
+
| --- | --- |
|
|
259
|
+
| `DSH_SSH_HELPER_DIR` | Override the plugin data directory. |
|
|
260
|
+
| `DSH_HOME` | Base directory for the default data path and the primary legacy import source. |
|
|
261
|
+
| `DSH_PROFILE_DIR` | Secondary legacy import source (`data/ssh-manager/servers.json`). |
|
|
262
|
+
|
|
263
|
+
### Remote host requirements
|
|
264
|
+
|
|
265
|
+
| Requirement | Why | Required |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| POSIX `sh`, `tar`, `find`, `du`, `df`, `stat`, `id`, `uname` | Backup, preflight, discovery, upload verification | yes |
|
|
268
|
+
| SFTP subsystem | File upload | only for `ssh_helper_upload` |
|
|
269
|
+
| `bash` | Update, restart and verify commands run under `bash -lc` | only for apply/rollback |
|
|
270
|
+
| `sha256sum` or `shasum -a 256` | Upload digest verification | only for upload |
|
|
271
|
+
| `git` | Reports remote/branch/commit/dirty during discovery and preflight | optional |
|
|
272
|
+
| `systemctl`, `docker`, `ss`/`netstat` | Raw service and port hints during discovery | optional |
|
|
273
|
+
|
|
274
|
+
### Known limitations
|
|
275
|
+
|
|
276
|
+
- **Single-file uploads only.** `ssh_helper_upload` pushes one regular file over SFTP. No directory sync, no resume, no remote deletion, 2 GiB ceiling.
|
|
277
|
+
- **No host-key pinning.** v1 accepts whatever key the server presents (see above). `known_hosts` verification is the most important missing control.
|
|
278
|
+
- **No cross-process locking.** Writes are serialized inside one host process and a per-server flag prevents two concurrent apply/rollback runs, but two DSH processes sharing the same data directory are not coordinated beyond atomic file replacement.
|
|
279
|
+
- **No in-panel update button, by design.** Applying and rolling back are agent-tool-only operations so that every write passes the approval gate.
|
|
280
|
+
- **No automatic account import.** Legacy import is a button, not a background migration.
|
|
281
|
+
- **Discovery never reads file contents.** It records directory structure, marker file names, git metadata and service names — never `.env`, config bodies or logs.
|
|
282
|
+
- **Backups are whole-directory archives.** A large `node_modules` or artifact directory inflates the archive and the free-space requirement.
|
|
283
|
+
- **Plans expire after 15 minutes** and at most 50 are cached; a stale plan id must be regenerated.
|
|
284
|
+
- **Output is truncated.** Commands producing more than 512 KiB of stdout lose the tail for that step; raise limits by making the command quieter.
|
|
285
|
+
- **Pre-1.0.** Tool names, data file shapes and panel layout may still change between minor versions.
|
|
286
|
+
|
|
287
|
+
## Relationship to `@local/dsh-ssh-manager`
|
|
288
|
+
|
|
289
|
+
The two plugins coexist. `dsh-ssh-manager` keeps its `ssh_servers` and `ssh_exec` tools and its own Settings page; this plugin uses an independent `ssh_helper_*` tool namespace and its own sidebar panel. The overlap is limited to a one-way, on-demand import of its `servers.json` account list — nothing is imported automatically, and nothing is shared afterwards.
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npm test # syntax check + node:test unit and in-process SSH end-to-end tests
|
|
295
|
+
npm run smoke # host-side smoke test, no network, no real data directory
|
|
296
|
+
npm run check # syntax check only
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The test suite runs a real `ssh2` server on loopback for the end-to-end cases (probe, scan, inspect, plan, apply, rollback, and a deliberately failing update that must keep its backup without auto-rollback).
|
|
300
|
+
|
|
301
|
+
```text
|
|
302
|
+
dsh-ssh-helper/
|
|
303
|
+
├── index.js # host entry: stores, services, gate, tools, prompt, API in one ctx.effect
|
|
304
|
+
├── client.js # sidebar panel (single file, React only, no build step)
|
|
305
|
+
├── cordis.patch.yml # bundle row: ssh-helper
|
|
306
|
+
├── host/
|
|
307
|
+
│ ├── accounts.js # account store, 0600, legacy import, redaction
|
|
308
|
+
│ ├── resources.js # deployment resource catalogue
|
|
309
|
+
│ ├── history.js # capped update history
|
|
310
|
+
│ ├── atomic.js # tmp+rename JSON storage with corrupt-file quarantine
|
|
311
|
+
│ ├── ssh.js # ssh2 exec / probe / SFTP upload, bounded capture
|
|
312
|
+
│ ├── shell.js # path validation, POSIX quoting, backup layout
|
|
313
|
+
│ ├── discover.js # read-only scan and inspect command builders
|
|
314
|
+
│ ├── update.js # plan / apply / rollback service
|
|
315
|
+
│ ├── upload.js # upload resolve / execute with server-side verification
|
|
316
|
+
│ ├── gate.js # the tools/pre-execute approval handler
|
|
317
|
+
│ ├── tools.js # the seven agent tools and their renderers
|
|
318
|
+
│ ├── prompt.js # ## SSH Helper system-prompt section
|
|
319
|
+
│ └── api.js # /ssh-helper/api prefix route
|
|
320
|
+
├── locale/ # plugin manager title and description (en, zh)
|
|
321
|
+
├── test/ # unit tests + in-process ssh2 server E2E
|
|
322
|
+
└── scripts/ # syntax check, smoke test
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Contributing
|
|
326
|
+
|
|
327
|
+
Issues and pull requests are welcome at [github.com/hunan36/dsh-ssh-helper](https://github.com/hunan36/dsh-ssh-helper). Please run `npm test` before opening a PR, and keep the property that no tool can write to a remote host without passing through `host/gate.js` — pull requests that add a `confirm`, `force` or `skip_approval` escape hatch will be declined.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
[MIT](LICENSE)
|