@elinpf/dsh-ops-tool-ssh 0.1.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 +4 -0
- package/README.md +37 -0
- package/README.zh.md +37 -0
- package/cordis.patch.yml +1 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.js +55 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +26 -0
- package/lib/types.d.ts +16 -0
- package/lib/types.js +8 -0
- package/package.json +59 -0
package/README.i18n.yaml
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @elinpf/dsh-ops-tool-ssh
|
|
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).
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
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.
|
|
8
|
+
|
|
9
|
+
- 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
|
+
|
|
13
|
+
## Design
|
|
14
|
+
|
|
15
|
+
This package is deliberately thin. All shared machinery — result shape `{ exitCode, stdout, stderr, command, error? }`, output schema, render, and the resolve-per-call execute template (30 s default timeout) — lives in `@elinpf/dsh-ops-shell-tool`. A consumer tool only supplies four identity pieces: tool name, resolved credential kind, profile-arg name, and `buildCommand`. The ops-access seam is resolved per call through `ctx.get('opsAccess')`, never a static inject (the preset mounts the group concurrently — a static inject deadlocks the loader).
|
|
16
|
+
|
|
17
|
+
- `src/index.ts` — the plugin (function plugin: `name`/`inject`/`Config`/`apply`, no default export). Registration goes through `ctx.effect`, so fiber disposal/HMR unloads the tool.
|
|
18
|
+
- `src/types.ts` — pure types (no runtime values).
|
|
19
|
+
- `src/invariant.ts` — invariant companion; no runtime invariant (stateless tool, owns no session events), registers package ownership only.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
- id: ops-tool-ssh
|
|
25
|
+
name: '@elinpf/dsh-ops-tool-ssh'
|
|
26
|
+
timeoutMs: 30000 # per-call shell timeout (ms)
|
|
27
|
+
connectTimeoutSeconds: 10 # ssh -o ConnectTimeout (TCP handshake wait)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Testing
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npm run build
|
|
34
|
+
npx vitest run
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Tests mount the plugin against a mock context (`tests/harness.ts`) that captures tool registrations, shell resolve/run calls, and effect disposers — covering command assembly, quoting, credential tokens, error fallbacks, render purity, and HMR unload.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @elinpf/dsh-ops-tool-ssh
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness 运维模式的 `ssh` 工具 — 使用已注册的 ssh 访问档案在远程主机上执行命令(密钥路径、端口、user@host 自动注入)。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
ops-access 凭据缝隙的消费方:模型用档案名加命令调用 `ssh`,插件经 `opsAccess` 解析档案,再经 `ctx.shell` 执行。`BatchMode=yes` 让任何需要交互的场景快速失败;`StrictHostKeyChecking=accept-new` 首次连接信任主机密钥、密钥变更则拒绝。可用 `list_access` 查看可选主机名。
|
|
8
|
+
|
|
9
|
+
- 远程命令作为**一个**单引号参数整体传出 — 管道、重定向、`&&`、`;`、`$()` 全部在远程主机执行,本地 shell 绝不切分这行命令(2026-08-27 险情:一条未加引号的 `&&` 链差一次认证失败就在本地删掉了控制面清单)。
|
|
10
|
+
- 只有密钥路径换成按次生成的凭据 token;user@host 和端口保持内联。展示命令(模型可见、入日志)只含 token — 真正执行的命令才带真实值。
|
|
11
|
+
- 信号死亡(exitCode 为 null)归一化为 -1,原因写入 `error` 字段。
|
|
12
|
+
|
|
13
|
+
## 设计
|
|
14
|
+
|
|
15
|
+
本包刻意保持单薄。所有共享机制 — 结果形状 `{ exitCode, stdout, stderr, command, error? }`、输出 schema、render、按次解析的执行模板(默认 30s 超时)— 都在 `@elinpf/dsh-ops-shell-tool`。消费方工具只提供四个身份要素:工具名、凭据 kind、档案参数名、`buildCommand`。ops-access 缝隙每次调用经 `ctx.get('opsAccess')` 现取,绝不用静态 inject(preset 并发挂载同组插件,静态 inject 会让加载器死锁)。
|
|
16
|
+
|
|
17
|
+
- `src/index.ts` — 插件本体(函数式插件:`name`/`inject`/`Config`/`apply`,无默认导出)。注册走 `ctx.effect`,fiber 销毁/HMR 时工具随之卸载。
|
|
18
|
+
- `src/types.ts` — 纯类型(无任何运行时值)。
|
|
19
|
+
- `src/invariant.ts` — invariant 伴随插件;无运行时 invariant(工具无状态,不拥有 session 事件),仅登记包的归属。
|
|
20
|
+
|
|
21
|
+
## 配置
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
- id: ops-tool-ssh
|
|
25
|
+
name: '@elinpf/dsh-ops-tool-ssh'
|
|
26
|
+
timeoutMs: 30000 # 单次调用的 shell 超时(毫秒)
|
|
27
|
+
connectTimeoutSeconds: 10 # ssh -o ConnectTimeout(TCP 握手等待)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 测试
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npm run build
|
|
34
|
+
npx vitest run
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
测试用 mock 上下文(`tests/harness.ts`)挂载插件,捕获工具注册、shell resolve/run 调用和 effect disposer — 覆盖命令拼装、引号、凭据 token、错误兜底、render 纯函数性和 HMR 卸载。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ops ssh tool consumer.
|
|
3
|
+
*
|
|
4
|
+
* The `ssh` tool: resolves an `ssh` profile through the ops-access seam and
|
|
5
|
+
* runs a remote command via ctx.shell, injecting the profile's key path,
|
|
6
|
+
* port, and user@host. The remote command is shell-quoted as ONE argument —
|
|
7
|
+
* the local shell must never split it: an unquoted && chain once came one
|
|
8
|
+
* auth failure short of deleting control-plane manifests remotely while
|
|
9
|
+
* `restoring` them on the LOCAL machine (2026-08-27 near-miss). All shared
|
|
10
|
+
* machinery (result shape, output schema, render, execute template) lives
|
|
11
|
+
* in @elinpf/dsh-ops-shell-tool.
|
|
12
|
+
*
|
|
13
|
+
* @module @elinpf/dsh-ops-tool-ssh
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import z from '@deepseek-ai/schemastery';
|
|
17
|
+
import type { SshToolConfig } from './types';
|
|
18
|
+
export type { SshToolConfig } from './types';
|
|
19
|
+
export declare const name = "ops-tool-ssh";
|
|
20
|
+
export declare const inject: string[];
|
|
21
|
+
export declare const Config: z<Schemastery.ObjectS<{
|
|
22
|
+
/** Per-call shell timeout for ssh runs (ms). */
|
|
23
|
+
timeoutMs: z<number, number>;
|
|
24
|
+
/** ssh -o ConnectTimeout value (seconds) — how long to wait for the TCP handshake. */
|
|
25
|
+
connectTimeoutSeconds: z<number, number>;
|
|
26
|
+
}>, Schemastery.ObjectT<{
|
|
27
|
+
/** Per-call shell timeout for ssh runs (ms). */
|
|
28
|
+
timeoutMs: z<number, number>;
|
|
29
|
+
/** ssh -o ConnectTimeout value (seconds) — how long to wait for the TCP handshake. */
|
|
30
|
+
connectTimeoutSeconds: z<number, number>;
|
|
31
|
+
}>>;
|
|
32
|
+
export declare function apply(ctx: Context, config: SshToolConfig): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ops ssh tool consumer.
|
|
3
|
+
*
|
|
4
|
+
* The `ssh` tool: resolves an `ssh` profile through the ops-access seam and
|
|
5
|
+
* runs a remote command via ctx.shell, injecting the profile's key path,
|
|
6
|
+
* port, and user@host. The remote command is shell-quoted as ONE argument —
|
|
7
|
+
* the local shell must never split it: an unquoted && chain once came one
|
|
8
|
+
* auth failure short of deleting control-plane manifests remotely while
|
|
9
|
+
* `restoring` them on the LOCAL machine (2026-08-27 near-miss). All shared
|
|
10
|
+
* machinery (result shape, output schema, render, execute template) lives
|
|
11
|
+
* in @elinpf/dsh-ops-shell-tool.
|
|
12
|
+
*
|
|
13
|
+
* @module @elinpf/dsh-ops-tool-ssh
|
|
14
|
+
*/
|
|
15
|
+
import z from '@deepseek-ai/schemastery';
|
|
16
|
+
import { registerProfiledShellTool, shellQuote } from '@elinpf/dsh-ops-shell-tool';
|
|
17
|
+
// ── Plugin identity ───────────────────────────────────────────────────────────
|
|
18
|
+
export const name = 'ops-tool-ssh';
|
|
19
|
+
export const inject = ['shell', 'tools'];
|
|
20
|
+
// ── Config ───────────────────────────────────────────────────────────────────
|
|
21
|
+
export const Config = z.object({
|
|
22
|
+
/** Per-call shell timeout for ssh runs (ms). */
|
|
23
|
+
timeoutMs: z.number().default(30000),
|
|
24
|
+
/** ssh -o ConnectTimeout value (seconds) — how long to wait for the TCP handshake. */
|
|
25
|
+
connectTimeoutSeconds: z.number().default(10),
|
|
26
|
+
});
|
|
27
|
+
// ── Plugin apply ─────────────────────────────────────────────────────────────
|
|
28
|
+
export function apply(ctx, config) {
|
|
29
|
+
registerProfiledShellTool(ctx, {
|
|
30
|
+
timeoutMs: config.timeoutMs,
|
|
31
|
+
name: 'ssh',
|
|
32
|
+
kind: 'ssh',
|
|
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.',
|
|
35
|
+
targetParamDescription: 'SSH host profile name. Use list_access to see options.',
|
|
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
|
+
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}`);
|
|
48
|
+
// The remote command goes out as ONE single-quoted argument: sshd
|
|
49
|
+
// re-runs it through the remote shell, where &&, pipes, redirects and
|
|
50
|
+
// $() all belong. Left unquoted, the LOCAL shell would split the line
|
|
51
|
+
// and run the later segments here as root (2026-08-27 near-miss).
|
|
52
|
+
return `ssh ${opts.join(' ')} ${user}@${host} ${shellQuote(command)}`;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant companion for @elinpf/dsh-ops-tool-ssh.
|
|
3
|
+
*
|
|
4
|
+
* @module @elinpf/dsh-ops-tool-ssh/invariant
|
|
5
|
+
*/
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
declare const name = "ops-tool-ssh-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Context carrying the invariant service.
|
|
13
|
+
* @returns a promise resolving after registration.
|
|
14
|
+
*/
|
|
15
|
+
declare const apply: (ctx: any) => Promise<void>;
|
|
16
|
+
export { apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant companion for @elinpf/dsh-ops-tool-ssh.
|
|
3
|
+
*
|
|
4
|
+
* @module @elinpf/dsh-ops-tool-ssh/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = '@elinpf/dsh-ops-tool-ssh';
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = 'ops-tool-ssh-invariant';
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ['invariants'];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this tool is fully stateless — it owns no session
|
|
13
|
+
* event types and no durable shape. Every call resolves its profile fresh
|
|
14
|
+
* through the ops-access seam and delegates execution to ctx.shell; there is
|
|
15
|
+
* nothing folded from the log and nothing cached to keep consistent.
|
|
16
|
+
*/
|
|
17
|
+
const install = () => { };
|
|
18
|
+
/**
|
|
19
|
+
* Register this package's invariant companion.
|
|
20
|
+
* @param ctx - Context carrying the invariant service.
|
|
21
|
+
* @returns a promise resolving after registration.
|
|
22
|
+
*/
|
|
23
|
+
const apply = async (ctx) => {
|
|
24
|
+
ctx.invariants.register(PACKAGE_NAME, install);
|
|
25
|
+
};
|
|
26
|
+
export { apply, inject, name };
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the ops-tool-ssh plugin.
|
|
3
|
+
*
|
|
4
|
+
* Types only — no runtime values live here.
|
|
5
|
+
*
|
|
6
|
+
* @module @elinpf/dsh-ops-tool-ssh
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Resolved plugin config (post-Config-schema defaults).
|
|
10
|
+
*/
|
|
11
|
+
export interface SshToolConfig {
|
|
12
|
+
/** Per-call shell timeout for ssh runs (ms). */
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
/** ssh -o ConnectTimeout value (seconds) — how long to wait for the TCP handshake. */
|
|
15
|
+
connectTimeoutSeconds: number;
|
|
16
|
+
}
|
package/lib/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elinpf/dsh-ops-tool-ssh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ssh tool consumer of the ops-access seam — resolves ssh profiles and runs commands via ctx.shell, injecting credential paths automatically.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./invariant": {
|
|
14
|
+
"types": "./lib/invariant.d.ts",
|
|
15
|
+
"default": "./lib/invariant.js"
|
|
16
|
+
},
|
|
17
|
+
"./types": {
|
|
18
|
+
"types": "./lib/types.d.ts",
|
|
19
|
+
"default": "./lib/types.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib/index.js",
|
|
25
|
+
"lib/invariant.js",
|
|
26
|
+
"lib/types.js",
|
|
27
|
+
"lib/**/*.d.ts",
|
|
28
|
+
"cordis.patch.yml"
|
|
29
|
+
],
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
37
|
+
"@elinpf/dsh-ops-shell-tool": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
44
|
+
"@deepseek-ai/dsh-shell": "0.0.1-rc.5",
|
|
45
|
+
"@deepseek-ai/dsh-tools": "0.0.1-rc.1",
|
|
46
|
+
"typescript": "^5.4.0",
|
|
47
|
+
"vitest": "^4.1.11",
|
|
48
|
+
"@elinpf/dsh-ops-access": "0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run"
|
|
58
|
+
}
|
|
59
|
+
}
|