@elinpf/dsh-ops-tool-ssh 0.1.7 → 0.2.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/README.i18n.yaml CHANGED
@@ -1,4 +1,4 @@
1
1
  title: "@elinpf/dsh-ops-tool-ssh"
2
2
  description:
3
- zh: "ssh 工具 — 解析 ssh 访问档案,经 ctx.shell 在远程主机执行命令"
4
- en: "ssh tool — resolves ssh access profiles and runs commands on remote hosts via ctx.shell"
3
+ zh: "ssh 工具 — 解析 ssh 访问档案,经 ctx.shell 在远程主机执行命令(密钥或 sshpass 密码登录)"
4
+ en: "ssh tool — resolves ssh access profiles and runs commands on remote hosts via ctx.shell (key or sshpass password auth)"
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # @elinpf/dsh-ops-tool-ssh
2
2
 
3
- The `ssh` tool for DeepSeek Harness ops mode — runs a command on a remote host over SSH, using a registered ssh access profile (key, port, user@host injected automatically).
3
+ The `ssh` tool for DeepSeek Harness ops mode — runs a command on a remote host over SSH, using a registered ssh access profile (credential, port, user@host injected automatically). Supports key auth and password auth (via `sshpass`).
4
4
 
5
5
  ## What it does
6
6
 
7
- A consumer of the ops-access credential seam: the model calls `ssh` with a profile name and a command; the plugin resolves the profile through `opsAccess` and runs the command via `ctx.shell`. `BatchMode=yes` makes anything that would prompt fail fast, and `StrictHostKeyChecking=accept-new` trusts a host key on first contact while refusing changed ones. Use `list_access` to see available host names.
7
+ A consumer of the ops-access credential seam: the model calls `ssh` with a profile name and a command; the plugin resolves the profile through `opsAccess` and runs the command via `ctx.shell`. The profile's credential usually comes from a referenced `ssh-cred` entry (registered once, shared by many hosts); the merge has already happened at resolve time, so this tool only ever sees flat fields.
8
8
 
9
+ - **Key auth** (default): `BatchMode=yes` makes anything that would prompt fail fast, and `StrictHostKeyChecking=accept-new` trusts a host key on first contact while refusing changed ones.
10
+ - **Password auth** (profile carries `password`): runs through `sshpass -f <password-file>` with `PreferredAuthentications=password`, `PubkeyAuthentication=no`, and `NumberOfPasswordPrompts=1` — BatchMode must stay OFF, since it would suppress the very prompt sshpass exists to answer. Requires `sshpass` installed on the dsh host. Meant for network devices and other appliances that only offer password login.
9
11
  - The remote command is passed as ONE single-quoted argument — pipes, redirects, `&&`, `;` and `$()` all execute on the REMOTE host; the local shell never splits the line (an unquoted `&&` chain once came one auth failure short of deleting control-plane manifests locally, 2026-08-27 near-miss).
10
- - Only the key path gets a per-call credential token; user@host and port stay inline. The display command (model-visible, logged) keeps tokens — only the executed command carries real values.
11
- - Signal deaths (null exitCode) are normalized to exitCode -1, with the cause surfaced in `error`.
12
+ - Only the key/password path gets a per-call credential token; user@host and port stay inline. The display command (model-visible, logged) keeps tokens — only the executed command carries real values.
13
+ - Signal deaths (null exitCode) are normalized to exitCode -1, with the cause surfaced in `error`. Use `list_access` to see available host names.
12
14
 
13
15
  ## Design
14
16
 
package/README.zh.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # @elinpf/dsh-ops-tool-ssh
2
2
 
3
- DeepSeek Harness 运维模式的 `ssh` 工具 — 使用已注册的 ssh 访问档案在远程主机上执行命令(密钥路径、端口、user@host 自动注入)。
3
+ DeepSeek Harness 运维模式的 `ssh` 工具 — 使用已注册的 ssh 访问档案在远程主机上执行命令(凭证、端口、user@host 自动注入)。支持密钥登录和密码登录(经 `sshpass`)
4
4
 
5
5
  ## 功能
6
6
 
7
- ops-access 凭据缝隙的消费方:模型用档案名加命令调用 `ssh`,插件经 `opsAccess` 解析档案,再经 `ctx.shell` 执行。`BatchMode=yes` 让任何需要交互的场景快速失败;`StrictHostKeyChecking=accept-new` 首次连接信任主机密钥、密钥变更则拒绝。可用 `list_access` 查看可选主机名。
7
+ ops-access 凭据缝隙的消费方:模型用档案名加命令调用 `ssh`,插件经 `opsAccess` 解析档案,再经 `ctx.shell` 执行。档案的凭证通常来自其引用的 `ssh-cred` 条目(登记一次、多机共享)——合并在 resolve 时已完成,本工具永远只看到扁平字段。
8
8
 
9
+ - **密钥登录**(默认):`BatchMode=yes` 让任何需要交互的场景快速失败;`StrictHostKeyChecking=accept-new` 首次连接信任主机密钥、密钥变更则拒绝。
10
+ - **密码登录**(档案带 `password`):经 `sshpass -f <密码文件>` 执行,带 `PreferredAuthentications=password`、`PubkeyAuthentication=no`、`NumberOfPasswordPrompts=1` — BatchMode 必须关闭,因为它会压制 sshpass 赖以应答的那个提示符。要求 dsh 宿主机上装有 `sshpass`。面向只提供密码登录的网络设备等场景。
9
11
  - 远程命令作为**一个**单引号参数整体传出 — 管道、重定向、`&&`、`;`、`$()` 全部在远程主机执行,本地 shell 绝不切分这行命令(2026-08-27 险情:一条未加引号的 `&&` 链差一次认证失败就在本地删掉了控制面清单)。
10
- - 只有密钥路径换成按次生成的凭据 token;user@host 和端口保持内联。展示命令(模型可见、入日志)只含 token — 真正执行的命令才带真实值。
11
- - 信号死亡(exitCode 为 null)归一化为 -1,原因写入 `error` 字段。
12
+ - 只有密钥/密码路径换成按次生成的凭据 token;user@host 和端口保持内联。展示命令(模型可见、入日志)只含 token — 真正执行的命令才带真实值。
13
+ - 信号死亡(exitCode 为 null)归一化为 -1,原因写入 `error` 字段。可用 `list_access` 查看可选主机名。
12
14
 
13
15
  ## 设计
14
16
 
package/lib/index.js CHANGED
@@ -31,25 +31,46 @@ export function apply(ctx, config) {
31
31
  name: 'ssh',
32
32
  kind: 'ssh',
33
33
  targetParam: 'host',
34
- description: 'Run a command on a remote host over SSH, using a registered ssh access profile (key, port, user@host injected automatically). Non-interactive: BatchMode is on, so anything that would prompt fails fast. Use list_access to see available host names.',
34
+ description: 'Run a command on a remote host over SSH, using a registered ssh access profile (credential — key from the profile or its referenced ssh-cred, port, user@host injected automatically; password profiles run through sshpass). Non-interactive: key auth uses BatchMode so anything that would prompt fails fast, password auth allows exactly one prompt answered by sshpass. Use list_access to see available host names.',
35
35
  targetParamDescription: 'SSH host profile name. Use list_access to see options.',
36
36
  commandDescription: 'Command to run on the remote host, e.g. "systemctl status ceph-osd@3". The whole string is passed as ONE shell-quoted argument and run by the remote shell: pipes, redirects, &&, ; and $() all execute on the REMOTE host — nothing is interpreted locally.',
37
37
  buildCommand(fields, command, ref) {
38
- const { host, user, key, port } = fields;
39
- // BatchMode: never prompt (password/passphrase)fail fast instead.
40
- // accept-new: trust a host key on first contact, refuse changed ones —
41
- // ops hosts are reached by name from the registry, not typed by hand.
42
- const opts = ['-o BatchMode=yes', `-o ConnectTimeout=${config.connectTimeoutSeconds}`, '-o StrictHostKeyChecking=accept-new'];
43
- // Only the key path gets a credential token; user@host/port stay inline.
44
- if (key !== undefined)
45
- opts.push(`-i ${ref('key')}`);
46
- if (port !== undefined)
47
- opts.push(`-p ${port}`);
38
+ const { host, user, key, port, password } = fields;
39
+ // user may live on the referenced ssh-cred entry core's reference
40
+ // expansion has already merged it in; core's validateResolved rejects
41
+ // user-less profiles at resolve time, so this is a belt-and-braces guard.
42
+ if (typeof user !== 'string' || user.length === 0) {
43
+ throw new Error('profile "' + String(fields.cred ?? host) + '" resolved without a login user — set user on the host entry or on its referenced ssh-cred entry');
44
+ }
45
+ const connOpts = [`-o ConnectTimeout=${config.connectTimeoutSeconds}`, '-o StrictHostKeyChecking=accept-new'];
48
46
  // The remote command goes out as ONE single-quoted argument: sshd
49
47
  // re-runs it through the remote shell, where &&, pipes, redirects and
50
48
  // $() all belong. Left unquoted, the LOCAL shell would split the line
51
49
  // and run the later segments here as root (2026-08-27 near-miss).
52
- return `ssh ${opts.join(' ')} ${user}@${host} ${shellQuote(command)}`;
50
+ const target = `${user}@${host} ${shellQuote(command)}`;
51
+ // Password auth: sshpass answers sshd's prompt with the first line of
52
+ // the password file. BatchMode must stay OFF — it suppresses the very
53
+ // prompt sshpass exists to answer. NumberOfPasswordPrompts=1 keeps a
54
+ // wrong password failing fast instead of looping; PubkeyAuthentication=no
55
+ // keeps a stray local key from silently winning over the intended
56
+ // password path. Requires sshpass installed on the dsh host.
57
+ if (password !== undefined) {
58
+ const opts = ['-o PreferredAuthentications=password', '-o PubkeyAuthentication=no', '-o NumberOfPasswordPrompts=1', ...connOpts];
59
+ if (port !== undefined)
60
+ opts.push(`-p ${port}`);
61
+ return `sshpass -f ${ref('password')} ssh ${opts.join(' ')} ${target}`;
62
+ }
63
+ // Key auth (or agent): BatchMode: never prompt (password/passphrase) —
64
+ // fail fast instead. accept-new: trust a host key on first contact,
65
+ // refuse changed ones — ops hosts are reached by name from the
66
+ // registry, not typed by hand. Only the key path gets a credential
67
+ // token; user@host/port stay inline.
68
+ const opts = ['-o BatchMode=yes', ...connOpts];
69
+ if (key !== undefined)
70
+ opts.push(`-i ${ref('key')}`);
71
+ if (port !== undefined)
72
+ opts.push(`-p ${port}`);
73
+ return `ssh ${opts.join(' ')} ${target}`;
53
74
  },
54
75
  });
55
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elinpf/dsh-ops-tool-ssh",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "ssh tool consumer of the ops-access seam — resolves ssh profiles and runs commands via ctx.shell, injecting credential paths automatically.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@deepseek-ai/schemastery": "^3.18.1",
37
- "@elinpf/dsh-ops-shell-tool": "^0.1.7"
37
+ "@elinpf/dsh-ops-shell-tool": "^0.2.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.1"
@@ -45,7 +45,7 @@
45
45
  "@deepseek-ai/dsh-tools": "0.0.1-rc.1",
46
46
  "typescript": "^5.4.0",
47
47
  "vitest": "^4.1.11",
48
- "@elinpf/dsh-ops-access": "0.1.7"
48
+ "@elinpf/dsh-ops-access": "0.2.0"
49
49
  },
50
50
  "license": "MIT",
51
51
  "publishConfig": {