@stardustlc/dsh-docker 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.en.md ADDED
@@ -0,0 +1,70 @@
1
+ [中文](README.md)
2
+
3
+ # dsh-docker
4
+
5
+ > **Your agent can manage containers now**: five tools covering container listing, logs, inspection, in-container exec, and lifecycle management.
6
+
7
+ DSH (DeepSeek Harness) container-management plugin: runs the docker CLI through the official subprocess service with shell-free argv arrays, an approval gate on `docker_exec`, and **zero runtime dependencies**.
8
+
9
+ ![npm version](https://img.shields.io/npm/v/dsh-docker?label=npm&color=blue) ![npm downloads](https://img.shields.io/npm/dm/dsh-docker) ![license](https://img.shields.io/npm/l/dsh-docker) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-docker?style=social)
10
+
11
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ dsh plugin --profile web add @stardustlc/dsh-docker
17
+ ```
18
+
19
+ Requires Docker installed locally (`docker version` should work); use `dockerPath` when it is not on PATH.
20
+
21
+ ## Configuration
22
+
23
+ ```yaml
24
+ - id: docker
25
+ name: '@stardustlc/dsh-docker'
26
+ config:
27
+ # dockerPath: C:\Program Files\Docker\Docker\resources\bin\docker.exe
28
+ timeoutMs: 60000 # per-operation timeout (default 60s, 5s - 10min)
29
+ # execApproval: false # disable the docker_exec approval gate (default true)
30
+ ```
31
+
32
+ ## Tools
33
+
34
+ | Tool | Purpose | Safety |
35
+ | :-- | :-- | :-- |
36
+ | `docker_ps` | List containers (status/image/state, filterable) | — |
37
+ | `docker_logs` | Tail container logs (line clamp, short follow) | — |
38
+ | `docker_inspect` | Container details (image/state/ports) | — |
39
+ | `docker_exec` | Run a command inside a container | Approval gate + container-name validation |
40
+ | `docker_manage` | start / stop / restart / rm | Destructive actions clearly labeled |
41
+
42
+ ### Examples
43
+
44
+ ```text
45
+ docker_ps {}
46
+ docker_ps { all: true, name: web }
47
+ docker_logs { container: web, tail: 200 }
48
+ docker_inspect { container: web }
49
+ docker_exec { container: web, command: 'df -h' }
50
+ docker_manage { container: web, action: restart }
51
+ ```
52
+
53
+ ## Safety
54
+
55
+ - **No shell**: every argument is its own argv element — command injection is impossible
56
+ - **Approval gate**: docker_exec asks first (mirroring dsh-email / dsh-sql); headless environments without an approval channel are denied
57
+ - **Container-name validation**: only `[A-Za-z0-9][A-Za-z0-9_.:-]*` accepted — no argument injection
58
+ - **Timeout clamps**: 5s - 10min per operation; follow mode capped at an extra 30s
59
+ - **Log clamping**: tail 1-2000 lines
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ pnpm install
65
+ pnpm test # build + 24 tests
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ [English](README.en.md)
2
+
3
+ # dsh-docker
4
+
5
+ > **你的 agent 会管容器了**:五个工具覆盖容器列表、日志、详情、容器内执行与生命周期管理。
6
+
7
+ DSH(DeepSeek Harness)容器管理插件:走官方 subprocess 服务跑 docker CLI,argv 数组无 shell 注入,`docker_exec` 默认审批门,**零运行时依赖**。
8
+
9
+ ![npm version](https://img.shields.io/npm/v/dsh-docker?label=npm&color=blue) ![npm downloads](https://img.shields.io/npm/dm/dsh-docker) ![license](https://img.shields.io/npm/l/dsh-docker) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-docker?style=social)
10
+
11
+ [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ dsh plugin --profile web add @stardustlc/dsh-docker
17
+ ```
18
+
19
+ 需要本机装有 Docker(`docker version` 能出结果即可);不在 PATH 上时用 `dockerPath` 指定。
20
+
21
+ ## 配置
22
+
23
+ ```yaml
24
+ - id: docker
25
+ name: '@stardustlc/dsh-docker'
26
+ config:
27
+ # dockerPath: C:\Program Files\Docker\Docker\resources\bin\docker.exe
28
+ timeoutMs: 60000 # 单次操作超时(默认 60 秒,5 秒 - 10 分钟)
29
+ # execApproval: false # 关闭 docker_exec 审批门(默认 true)
30
+ ```
31
+
32
+ ## 工具一览
33
+
34
+ | 工具 | 作用 | 安全 |
35
+ | :-- | :-- | :-- |
36
+ | `docker_ps` | 列出容器(状态/镜像/运行态,可过滤)| — |
37
+ | `docker_logs` | 查看日志尾部(行数钳制,可短时 follow)| — |
38
+ | `docker_inspect` | 容器详情(镜像/状态/端口)| — |
39
+ | `docker_exec` | 容器内执行命令 | 审批门 + 容器名白名单校验 |
40
+ | `docker_manage` | start / stop / restart / rm | 明确提示破坏性 |
41
+
42
+ ### 示例
43
+
44
+ ```text
45
+ docker_ps {}
46
+ docker_ps { all: true, name: web }
47
+ docker_logs { container: web, tail: 200 }
48
+ docker_inspect { container: web }
49
+ docker_exec { container: web, command: 'df -h' }
50
+ docker_manage { container: web, action: restart }
51
+ ```
52
+
53
+ ## 安全设计
54
+
55
+ - **无 shell**:全部参数独立 argv 数组,命令注入不可能
56
+ - **审批门**:docker_exec 默认弹审批(对齐 dsh-email / dsh-sql),headless 无审批通道时拒绝
57
+ - **容器名校验**:只允许 `[A-Za-z0-9][A-Za-z0-9_.:-]*`,杜绝参数注入
58
+ - **超时钳制**:单次操作 5 秒 - 10 分钟;follow 模式额外限 30 秒
59
+ - **日志钳制**:tail 1-2000 行
60
+
61
+ ## 开发
62
+
63
+ ```bash
64
+ pnpm install
65
+ pnpm test # 构建 + 24 个测试
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,17 @@
1
+ # dsh-docker 组合包补丁:把容器管理工具插件行插入 profile。
2
+ # 本补丁按 id 覆盖;用户应在自己 profile 的 cordis.patch.yml 里覆盖本行的整个
3
+ # config(见 README 的配置示例)。配置缺失时插件仍会加载(依赖 PATH 上的 docker)。
4
+ #
5
+ # 示例(用户 profile 的 cordis.patch.yml):
6
+ # - id: docker
7
+ # name: '@stardustlc/dsh-docker'
8
+ # config:
9
+ # # dockerPath: C:\Program Files\Docker\Docker\resources\bin\docker.exe
10
+ # timeoutMs: 60000 # 单次操作超时(默认 60 秒,5 秒 - 10 分钟)
11
+ # # execApproval: false # 关闭 docker_exec 的审批门(默认 true)
12
+ #
13
+ - insert:
14
+ - id: docker
15
+ name: '@stardustlc/dsh-docker'
16
+ config:
17
+ timeoutMs: 60000
package/lib/args.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * docker CLI argv 构建器:纯函数,argv 数组、无 shell。
3
+ *
4
+ * @module dsh-docker/args
5
+ */
6
+ /** 列出容器(JSON 格式,全部或按名称过滤)。 */
7
+ export declare function psArgs(docker: string, all: boolean, filter?: string): string[];
8
+ /** 查看日志(tail 钳制)。 */
9
+ export declare function logsArgs(docker: string, container: string, tail: number, follow: boolean): string[];
10
+ /** 查看详情(docker inspect 原生 JSON)。 */
11
+ export declare function inspectArgs(docker: string, container: string): string[];
12
+ /** 容器内执行命令。 */
13
+ export declare function execArgs(docker: string, container: string, command: string[]): string[];
14
+ /** 生命周期管理:start/stop/restart/rm。 */
15
+ export declare function manageArgs(docker: string, action: 'start' | 'stop' | 'restart' | 'rm', container: string): string[];
16
+ /** 拆分 exec 的命令字符串(按空白,支持引号)。 */
17
+ export declare function splitCommand(command: string): string[];
package/lib/args.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * docker CLI argv 构建器:纯函数,argv 数组、无 shell。
3
+ *
4
+ * @module dsh-docker/args
5
+ */
6
+ /** 列出容器(JSON 格式,全部或按名称过滤)。 */
7
+ export function psArgs(docker, all, filter) {
8
+ const argv = [docker, 'ps', all ? '--all' : '', '--format', 'json'].filter((item) => item !== '');
9
+ if (filter !== undefined && filter !== '')
10
+ argv.push('--filter', 'name=' + filter);
11
+ return argv;
12
+ }
13
+ /** 查看日志(tail 钳制)。 */
14
+ export function logsArgs(docker, container, tail, follow) {
15
+ return [docker, 'logs', follow ? '--follow' : '', '--tail', String(tail), container].filter((item) => item !== '');
16
+ }
17
+ /** 查看详情(docker inspect 原生 JSON)。 */
18
+ export function inspectArgs(docker, container) {
19
+ return [docker, 'inspect', container];
20
+ }
21
+ /** 容器内执行命令。 */
22
+ export function execArgs(docker, container, command) {
23
+ return [docker, 'exec', container, ...command];
24
+ }
25
+ /** 生命周期管理:start/stop/restart/rm。 */
26
+ export function manageArgs(docker, action, container) {
27
+ const flags = action === 'rm' ? ['rm', '--force'] : [action];
28
+ return [docker, ...flags, container];
29
+ }
30
+ /** 拆分 exec 的命令字符串(按空白,支持引号)。 */
31
+ export function splitCommand(command) {
32
+ const parts = [];
33
+ const matches = command.match(/"[^"]*"|'[^']*'|\S+/g);
34
+ if (matches === null)
35
+ return [];
36
+ return matches.map((part) => part.replace(/^["']|["']$/g, ''));
37
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * dsh-docker 配置解析:docker CLI 路径、超时与 exec 审批策略。
3
+ *
4
+ * @module dsh-docker/config
5
+ */
6
+ /** 插件行配置(cordis.patch.yml 里的 config 段,可缺省)。 */
7
+ export interface DockerConfig {
8
+ dockerPath?: string;
9
+ timeoutMs?: number;
10
+ graceMs?: number;
11
+ execApproval?: boolean;
12
+ }
13
+ /** 解析后的配置。 */
14
+ export interface ResolvedDockerConfig {
15
+ dockerPath: string;
16
+ timeoutMs: number;
17
+ graceMs: number;
18
+ execApproval: boolean;
19
+ }
20
+ /**
21
+ * 解析并校验配置。
22
+ */
23
+ export declare function resolveConfig(config: DockerConfig | undefined | null): ResolvedDockerConfig;
24
+ /** 校验容器名/ID,防参数注入(只允许字母数字与 _-.:/ 之外的拒绝)。 */
25
+ export declare function assertContainerRef(value: string): string;
package/lib/config.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * dsh-docker 配置解析:docker CLI 路径、超时与 exec 审批策略。
3
+ *
4
+ * @module dsh-docker/config
5
+ */
6
+ /**
7
+ * 解析并校验配置。
8
+ */
9
+ export function resolveConfig(config) {
10
+ const cfg = config ?? {};
11
+ const dockerPath = typeof cfg.dockerPath === 'string' && cfg.dockerPath.trim() !== '' ? cfg.dockerPath.trim() : 'docker';
12
+ let timeoutMs = 60000;
13
+ if (cfg.timeoutMs !== undefined) {
14
+ if (typeof cfg.timeoutMs !== 'number' || !Number.isFinite(cfg.timeoutMs) || cfg.timeoutMs <= 0)
15
+ throw new Error('timeoutMs 必须是大于 0 的数字(毫秒)。');
16
+ timeoutMs = Math.min(10 * 60 * 1000, Math.max(5000, Math.round(cfg.timeoutMs)));
17
+ }
18
+ let graceMs = 15000;
19
+ if (cfg.graceMs !== undefined) {
20
+ if (typeof cfg.graceMs !== 'number' || !Number.isFinite(cfg.graceMs) || cfg.graceMs <= 0)
21
+ throw new Error('graceMs 必须是大于 0 的数字(毫秒)。');
22
+ graceMs = Math.min(120000, Math.max(1000, Math.round(cfg.graceMs)));
23
+ }
24
+ const execApproval = cfg.execApproval !== false;
25
+ return { dockerPath, timeoutMs, graceMs, execApproval };
26
+ }
27
+ /** 校验容器名/ID,防参数注入(只允许字母数字与 _-.:/ 之外的拒绝)。 */
28
+ export function assertContainerRef(value) {
29
+ const trimmed = value.trim();
30
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(trimmed)) {
31
+ throw new Error('容器名/ID 非法(只允许字母数字开头,后续可含 _ . : -):' + value);
32
+ }
33
+ return trimmed;
34
+ }
package/lib/exec.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 进程执行层:把 DSH 官方 subprocess 服务包装成 Promise 式 ProcessRunner(与 dsh-ffmpeg 同款)。
3
+ * 全程 argv 数组、无 shell 解释。
4
+ *
5
+ * @module dsh-docker/exec
6
+ */
7
+ export interface RunResult {
8
+ exitCode: number | null;
9
+ signal: string | null;
10
+ stdout: string;
11
+ stderr: string;
12
+ }
13
+ export interface ProcessRunner {
14
+ run(argv: readonly string[], options?: {
15
+ timeoutMs?: number;
16
+ }): Promise<RunResult>;
17
+ }
18
+ export interface SubprocessHandleLike {
19
+ done: Promise<{
20
+ exitCode: number | null;
21
+ signal: string | null;
22
+ }>;
23
+ collected: {
24
+ stdout?: {
25
+ readFrom(offset: number): {
26
+ text: string;
27
+ };
28
+ };
29
+ stderr?: {
30
+ readFrom(offset: number): {
31
+ text: string;
32
+ };
33
+ };
34
+ };
35
+ terminate(): void;
36
+ }
37
+ export interface SubprocessSpawnLike {
38
+ (spec: {
39
+ argv: readonly string[];
40
+ cwd: string;
41
+ stdio: {
42
+ stdin: 'ignore';
43
+ stdout: {
44
+ maxBytes: number;
45
+ };
46
+ stderr: {
47
+ maxBytes: number;
48
+ };
49
+ };
50
+ graceMs: number;
51
+ signal?: AbortSignal;
52
+ }): SubprocessHandleLike;
53
+ }
54
+ /** 用 DSH subprocess 服务构造 ProcessRunner。 */
55
+ export declare function createSubprocessRunner(spawn: SubprocessSpawnLike, graceMs: number, defaultTimeoutMs: number): ProcessRunner;
package/lib/exec.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 进程执行层:把 DSH 官方 subprocess 服务包装成 Promise 式 ProcessRunner(与 dsh-ffmpeg 同款)。
3
+ * 全程 argv 数组、无 shell 解释。
4
+ *
5
+ * @module dsh-docker/exec
6
+ */
7
+ const COLLECT_BYTES = 4 * 1024 * 1024;
8
+ /** 用 DSH subprocess 服务构造 ProcessRunner。 */
9
+ export function createSubprocessRunner(spawn, graceMs, defaultTimeoutMs) {
10
+ return {
11
+ async run(argv, options) {
12
+ const timeoutMs = options?.timeoutMs ?? defaultTimeoutMs;
13
+ const controller = new AbortController();
14
+ const timer = setTimeout(() => controller.abort(new Error('docker operation timed out')), timeoutMs);
15
+ let handle;
16
+ try {
17
+ handle = spawn({
18
+ argv,
19
+ cwd: process.cwd(),
20
+ stdio: {
21
+ stdin: 'ignore',
22
+ stdout: { maxBytes: COLLECT_BYTES },
23
+ stderr: { maxBytes: COLLECT_BYTES },
24
+ },
25
+ graceMs,
26
+ signal: controller.signal,
27
+ });
28
+ }
29
+ finally {
30
+ clearTimeout(timer);
31
+ }
32
+ const outcome = await handle.done;
33
+ const stdout = handle.collected.stdout?.readFrom(0).text ?? '';
34
+ const stderr = handle.collected.stderr?.readFrom(0).text ?? '';
35
+ return { exitCode: outcome.exitCode, signal: outcome.signal, stdout, stderr };
36
+ },
37
+ };
38
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * dsh-docker —— 容器管理工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册五个面向模型的工具(docker_ps / docker_logs /
5
+ * docker_inspect / docker_exec / docker_manage)。进程执行走 DSH 官方 subprocess 服务
6
+ * (argv 数组、无 shell),docker_exec 默认走宿主审批门。零运行时依赖。
7
+ *
8
+ * @module dsh-docker
9
+ */
10
+ import { type DockerConfig } from './config.js';
11
+ import { type SubprocessSpawnLike } from './exec.js';
12
+ import { type DockerToolDefinition } from './tools.js';
13
+ /** cordis 服务注入:apply 里要用 ctx.subprocess 与 ctx.tools。 */
14
+ export declare const inject: string[];
15
+ /** 审批服务最小面。 */
16
+ export interface DockerApproval {
17
+ request(options: {
18
+ agent?: unknown;
19
+ toolName?: unknown;
20
+ callId?: unknown;
21
+ reason: string;
22
+ signal?: unknown;
23
+ }): Promise<'allowed-once' | 'cancelled' | 'unavailable' | string>;
24
+ }
25
+ /** 插件所需的最小 ctx 面。 */
26
+ export interface DockerPluginContext {
27
+ subprocess: {
28
+ spawn: SubprocessSpawnLike;
29
+ };
30
+ tools: {
31
+ register(definition: DockerToolDefinition, options?: {
32
+ prepend?: boolean;
33
+ }): () => void;
34
+ };
35
+ get?(name: 'approval'): DockerApproval | undefined;
36
+ on?(event: string, listener: () => void): () => void;
37
+ }
38
+ /**
39
+ * 插件入口:解析配置、封装执行器、注册五工具;docker_exec 注入审批门。
40
+ */
41
+ export declare function apply(ctx: DockerPluginContext, config?: DockerConfig | null): void;
42
+ export * from './args.js';
43
+ export * from './config.js';
44
+ export * from './exec.js';
45
+ export * from './parse.js';
46
+ export * from './tools.js';
package/lib/index.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * dsh-docker —— 容器管理工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册五个面向模型的工具(docker_ps / docker_logs /
5
+ * docker_inspect / docker_exec / docker_manage)。进程执行走 DSH 官方 subprocess 服务
6
+ * (argv 数组、无 shell),docker_exec 默认走宿主审批门。零运行时依赖。
7
+ *
8
+ * @module dsh-docker
9
+ */
10
+ import { resolveConfig } from './config.js';
11
+ import { createSubprocessRunner } from './exec.js';
12
+ import { buildDockerTools } from './tools.js';
13
+ /** cordis 服务注入:apply 里要用 ctx.subprocess 与 ctx.tools。 */
14
+ export const inject = ['subprocess', 'tools'];
15
+ /**
16
+ * 插件入口:解析配置、封装执行器、注册五工具;docker_exec 注入审批门。
17
+ */
18
+ export function apply(ctx, config) {
19
+ let cfg;
20
+ try {
21
+ cfg = resolveConfig(config);
22
+ }
23
+ catch (error) {
24
+ console.warn('[dsh-docker] ' + (error instanceof Error ? error.message : String(error)));
25
+ cfg = resolveConfig(null);
26
+ }
27
+ const runner = createSubprocessRunner(ctx.subprocess.spawn, cfg.graceMs, cfg.timeoutMs);
28
+ const tools = buildDockerTools(cfg, runner);
29
+ const disposers = [];
30
+ for (const definition of tools) {
31
+ if (definition.name === 'docker_exec' && cfg.execApproval) {
32
+ definition.gate = async (exec, next) => {
33
+ const approval = ctx.get?.('approval');
34
+ if (approval === undefined) {
35
+ return { kind: 'deny', reason: 'docker_exec 需要确认,但当前环境没有审批通道(如 headless)。如确定安全,可在配置中设置 execApproval: false 后直接执行。' };
36
+ }
37
+ const record = (typeof exec === 'object' && exec !== null ? exec : {});
38
+ const args = (typeof record.args === 'object' && record.args !== null ? record.args : {});
39
+ const command = typeof args.command === 'string' ? args.command : '';
40
+ const container = typeof args.container === 'string' ? args.container : '';
41
+ const outcome = await approval.request({
42
+ agent: record.agent,
43
+ toolName: record.name,
44
+ callId: record.callId,
45
+ reason: '在容器 ' + container + ' 内执行:' + command.slice(0, 200),
46
+ signal: record.signal,
47
+ });
48
+ if (outcome === 'allowed-once')
49
+ return next();
50
+ if (outcome === 'cancelled')
51
+ return { kind: 'deny', reason: '容器内执行确认被取消,命令未执行。' };
52
+ if (outcome === 'unavailable')
53
+ return { kind: 'deny', reason: '容器内执行确认不可用(没有可用的审批界面),命令未执行。' };
54
+ return { kind: 'deny', reason: '容器内执行未获批准:要么你拒绝了,要么当前会话处于 Full Access(审批策略 never)。若在 Full Access:切到 Read Only / Write 再执行,或关闭 execApproval(自行承担风险)。' };
55
+ };
56
+ }
57
+ disposers.push(ctx.tools.register(definition, { prepend: true }));
58
+ }
59
+ if (typeof ctx.on === 'function') {
60
+ ctx.on('dispose', () => {
61
+ for (const dispose of disposers)
62
+ dispose();
63
+ });
64
+ }
65
+ }
66
+ export * from './args.js';
67
+ export * from './config.js';
68
+ export * from './exec.js';
69
+ export * from './parse.js';
70
+ export * from './tools.js';
package/lib/parse.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * docker 输出解析:ps JSON 行 → 容器列表;inspect JSON → 摘要。
3
+ *
4
+ * @module dsh-docker/parse
5
+ */
6
+ /** 容器摘要。 */
7
+ export interface ContainerInfo {
8
+ id: string;
9
+ name: string;
10
+ image: string;
11
+ status: string;
12
+ state: string;
13
+ }
14
+ /** docker ps --format json 的每一行解析。 */
15
+ export declare function parsePsJson(text: string): ContainerInfo[];
16
+ /** docker inspect 的 JSON 摘要。 */
17
+ export declare function parseInspectJson(text: string): Record<string, unknown>;
package/lib/parse.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * docker 输出解析:ps JSON 行 → 容器列表;inspect JSON → 摘要。
3
+ *
4
+ * @module dsh-docker/parse
5
+ */
6
+ /** docker ps --format json 的每一行解析。 */
7
+ export function parsePsJson(text) {
8
+ const rows = [];
9
+ for (const line of text.split(/\r?\n/)) {
10
+ const trimmed = line.trim();
11
+ if (trimmed === '')
12
+ continue;
13
+ try {
14
+ const raw = JSON.parse(trimmed);
15
+ rows.push({
16
+ id: typeof raw.ID === 'string' ? raw.ID : String(raw.ID ?? ''),
17
+ name: typeof raw.Names === 'string' ? raw.Names : String(raw.Names ?? ''),
18
+ image: typeof raw.Image === 'string' ? raw.Image : String(raw.Image ?? ''),
19
+ status: typeof raw.Status === 'string' ? raw.Status : String(raw.Status ?? ''),
20
+ state: typeof raw.State === 'string' ? raw.State : String(raw.State ?? ''),
21
+ });
22
+ }
23
+ catch { /* 跳过坏行 */ }
24
+ }
25
+ return rows;
26
+ }
27
+ /** docker inspect 的 JSON 摘要。 */
28
+ export function parseInspectJson(text) {
29
+ const raw = JSON.parse(text);
30
+ if (!Array.isArray(raw) || raw.length === 0)
31
+ return {};
32
+ const first = raw[0];
33
+ const config = (typeof first.Config === 'object' && first.Config !== null ? first.Config : {});
34
+ const networkSettings = (typeof first.NetworkSettings === 'object' && first.NetworkSettings !== null ? first.NetworkSettings : {});
35
+ const ports = Array.isArray(networkSettings.Ports) ? networkSettings.Ports : [];
36
+ const portList = ports.filter((port) => port.PublicPort !== undefined && port.PublicPort !== null);
37
+ return {
38
+ id: typeof first.Id === 'string' ? first.Id.slice(0, 12) : String(first.Id ?? '').slice(0, 12),
39
+ name: typeof first.Name === 'string' ? first.Name.replace(/^\//, '') : '',
40
+ image: typeof config.Image === 'string' ? config.Image : '',
41
+ state: (typeof first.State === 'object' && first.State !== null ? first.State.Status : '') ?? '',
42
+ running: (typeof first.State === 'object' && first.State !== null ? first.State.Running : false) === true,
43
+ startedAt: (typeof first.State === 'object' && first.State !== null ? first.State.StartedAt : '') ?? '',
44
+ ports: portList.map((port) => String(port.PublicPort) + '/' + String(port.Type ?? 'tcp')),
45
+ };
46
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { type ResolvedDockerConfig } from './config.js';
2
+ import { type ProcessRunner } from './exec.js';
3
+ export interface ContentBlock {
4
+ type: 'text';
5
+ text: string;
6
+ }
7
+ export interface DockerToolDefinition {
8
+ name: string;
9
+ description: string;
10
+ parameters: {
11
+ type: 'object';
12
+ properties: Record<string, unknown>;
13
+ required?: string[];
14
+ };
15
+ output: {
16
+ schema: Record<string, unknown>;
17
+ render(args: unknown, value: unknown): ContentBlock[];
18
+ };
19
+ execute(args: unknown, exec: unknown): Promise<unknown>;
20
+ gate?(exec: unknown, next: () => Promise<unknown>): Promise<unknown>;
21
+ timeoutMs?: number;
22
+ }
23
+ /** 构建五个工具定义。 */
24
+ export declare function buildDockerTools(config: ResolvedDockerConfig, runner: ProcessRunner): DockerToolDefinition[];
package/lib/tools.js ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * 五个面向模型的容器工具:docker_ps / docker_logs / docker_inspect / docker_exec / docker_manage。
3
+ *
4
+ * @module dsh-docker/tools
5
+ */
6
+ import { inspectArgs, logsArgs, manageArgs, execArgs, psArgs, splitCommand } from './args.js';
7
+ import { assertContainerRef } from './config.js';
8
+ import { parseInspectJson, parsePsJson } from './parse.js';
9
+ function compileParameters(spec) {
10
+ const properties = {};
11
+ const required = [];
12
+ for (const [key, prop] of Object.entries(spec)) {
13
+ if (prop?.required === true)
14
+ required.push(key);
15
+ const node = {};
16
+ if (typeof prop?.type === 'string')
17
+ node.type = prop.type;
18
+ if (typeof prop?.description === 'string')
19
+ node.description = prop.description;
20
+ properties[key] = node;
21
+ }
22
+ return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
23
+ }
24
+ function asRecord(value) {
25
+ return typeof value === 'object' && value !== null ? value : {};
26
+ }
27
+ function optionalString(args, key) {
28
+ const value = args[key];
29
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
30
+ }
31
+ function requiredString(args, key, label) {
32
+ const value = optionalString(args, key);
33
+ if (value === undefined)
34
+ throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
35
+ return value;
36
+ }
37
+ async function runChecked(runner, argv, timeoutMs, label) {
38
+ const result = await runner.run(argv, { timeoutMs });
39
+ if (result.exitCode !== 0) {
40
+ const tail = result.stderr.trim().split(/\r?\n/).slice(-6).join(' | ');
41
+ throw new Error(label + '失败(退出码 ' + String(result.exitCode ?? 'null') + '):' + (tail || '无错误输出') + '。请确认 docker 可用(docker version)。');
42
+ }
43
+ return result;
44
+ }
45
+ const containerSchema = {
46
+ type: 'object',
47
+ properties: { id: { type: 'string' }, name: { type: 'string' }, image: { type: 'string' }, status: { type: 'string' }, state: { type: 'string' } },
48
+ additionalProperties: true,
49
+ };
50
+ const psSchema = {
51
+ type: 'object',
52
+ properties: { count: { type: 'integer' }, all: { type: 'boolean' }, containers: { type: 'array', items: containerSchema } },
53
+ additionalProperties: true,
54
+ };
55
+ const logsSchema = {
56
+ type: 'object',
57
+ properties: { container: { type: 'string' }, tail: { type: 'integer' }, text: { type: 'string' } },
58
+ additionalProperties: true,
59
+ };
60
+ const inspectSchema = {
61
+ type: 'object',
62
+ properties: { container: { type: 'string' }, info: { type: 'object', additionalProperties: true } },
63
+ additionalProperties: true,
64
+ };
65
+ const execSchema = {
66
+ type: 'object',
67
+ properties: { container: { type: 'string' }, exitCode: { type: 'integer' }, stdout: { type: 'string' } },
68
+ additionalProperties: true,
69
+ };
70
+ const manageSchema = {
71
+ type: 'object',
72
+ properties: { container: { type: 'string' }, action: { type: 'string' }, output: { type: 'string' } },
73
+ additionalProperties: true,
74
+ };
75
+ /** 构建五个工具定义。 */
76
+ export function buildDockerTools(config, runner) {
77
+ const cfg = config;
78
+ const timeout = cfg.timeoutMs;
79
+ const dockerPs = {
80
+ name: 'docker_ps',
81
+ description: '列出 Docker 容器(含状态/镜像/运行状态)。all=true 时包含已停止容器;name 可按容器名过滤。',
82
+ parameters: compileParameters({
83
+ all: { type: 'boolean', description: '是否包含已停止容器(默认 false)。' },
84
+ name: { type: 'string', description: '按容器名过滤(可选,子串匹配)。' },
85
+ }),
86
+ output: {
87
+ schema: psSchema,
88
+ render: (_args, value) => {
89
+ const rec = asRecord(value);
90
+ const containers = Array.isArray(rec.containers) ? rec.containers : [];
91
+ const lines = ['共 ' + containers.length + ' 个容器:'];
92
+ for (const item of containers) {
93
+ const c = asRecord(item);
94
+ lines.push('- ' + c.name + '(' + c.image + ',' + c.state + ')');
95
+ }
96
+ return [{ type: 'text', text: lines.join('\n') }];
97
+ },
98
+ },
99
+ async execute(rawArgs) {
100
+ const args = asRecord(rawArgs);
101
+ const all = args.all === true;
102
+ const result = await runChecked(runner, psArgs(cfg.dockerPath, all, optionalString(args, 'name')), timeout, 'docker ps');
103
+ const containers = parsePsJson(result.stdout);
104
+ return { count: containers.length, all, containers };
105
+ },
106
+ timeoutMs: timeout,
107
+ };
108
+ const dockerLogs = {
109
+ name: 'docker_logs',
110
+ description: '查看容器日志尾部。container 必填;tail 为行数(1-2000,默认 100);follow=true 时持续跟踪(受超时限制,一般用于短时观察)。',
111
+ parameters: compileParameters({
112
+ container: { type: 'string', required: true, description: '容器名或 ID(必填)。' },
113
+ tail: { type: 'integer', description: '日志行数 1-2000(默认 100)。' },
114
+ follow: { type: 'boolean', description: '是否持续跟踪(默认 false,受超时限制)。' },
115
+ }),
116
+ output: {
117
+ schema: logsSchema,
118
+ render: (_args, value) => {
119
+ const rec = asRecord(value);
120
+ return [{ type: 'text', text: '容器 ' + rec.container + ' 最近日志:\n' + rec.text }];
121
+ },
122
+ },
123
+ async execute(rawArgs) {
124
+ const args = asRecord(rawArgs);
125
+ const container = assertContainerRef(requiredString(args, 'container', '容器名'));
126
+ const tailRaw = args.tail;
127
+ const tail = typeof tailRaw === 'number' && Number.isInteger(tailRaw) ? Math.min(2000, Math.max(1, tailRaw)) : 100;
128
+ const follow = args.follow === true;
129
+ const result = await runChecked(runner, logsArgs(cfg.dockerPath, container, tail, follow), follow ? Math.min(timeout, 30000) : timeout, 'docker logs');
130
+ return { container, tail, text: result.stdout };
131
+ },
132
+ timeoutMs: timeout,
133
+ };
134
+ const dockerInspect = {
135
+ name: 'docker_inspect',
136
+ description: '查看容器详情:镜像、运行状态、启动时间、端口映射。',
137
+ parameters: compileParameters({
138
+ container: { type: 'string', required: true, description: '容器名或 ID(必填)。' },
139
+ }),
140
+ output: {
141
+ schema: inspectSchema,
142
+ render: (_args, value) => {
143
+ const rec = asRecord(value);
144
+ const info = asRecord(rec.info);
145
+ return [{ type: 'text', text: '容器 ' + info.name + '(' + info.image + '):' + (info.running === true ? '运行中' : '已停止') + ',端口:' + (Array.isArray(info.ports) ? info.ports.join(', ') : '无') }];
146
+ },
147
+ },
148
+ async execute(rawArgs) {
149
+ const args = asRecord(rawArgs);
150
+ const container = assertContainerRef(requiredString(args, 'container', '容器名'));
151
+ const result = await runChecked(runner, inspectArgs(cfg.dockerPath, container), timeout, 'docker inspect');
152
+ return { container, info: parseInspectJson(result.stdout) };
153
+ },
154
+ timeoutMs: timeout,
155
+ };
156
+ const dockerExec = {
157
+ name: 'docker_exec',
158
+ description: '在容器内执行命令(默认走审批门)。command 为命令字符串(按空白拆分,支持引号);返回退出码与 stdout(stderr 并入错误提示)。',
159
+ parameters: compileParameters({
160
+ container: { type: 'string', required: true, description: '容器名或 ID(必填)。' },
161
+ command: { type: 'string', required: true, description: '要执行的命令,如 ls -la 或 sh -c \'cat /etc/hosts\'(必填)。' },
162
+ }),
163
+ output: {
164
+ schema: execSchema,
165
+ render: (_args, value) => {
166
+ const rec = asRecord(value);
167
+ return [{ type: 'text', text: '容器 ' + rec.container + ' 执行完成(退出码 ' + rec.exitCode + '):\n' + rec.stdout }];
168
+ },
169
+ },
170
+ async execute(rawArgs) {
171
+ const args = asRecord(rawArgs);
172
+ const container = assertContainerRef(requiredString(args, 'container', '容器名'));
173
+ const command = splitCommand(requiredString(args, 'command', '命令'));
174
+ if (command.length === 0)
175
+ throw new Error('command 不能为空。');
176
+ const result = await runChecked(runner, execArgs(cfg.dockerPath, container, command), timeout, 'docker exec');
177
+ return { container, exitCode: result.exitCode, stdout: result.stdout };
178
+ },
179
+ gate(exec, next) {
180
+ return next();
181
+ },
182
+ timeoutMs: timeout,
183
+ };
184
+ const dockerManage = {
185
+ name: 'docker_manage',
186
+ description: '容器生命周期管理:start / stop / restart / rm(rm 强制删除)。注意 stop/restart/rm 会中断容器,谨慎使用。',
187
+ parameters: compileParameters({
188
+ container: { type: 'string', required: true, description: '容器名或 ID(必填)。' },
189
+ action: { type: 'string', required: true, description: '操作:start / stop / restart / rm(必填)。' },
190
+ }),
191
+ output: {
192
+ schema: manageSchema,
193
+ render: (_args, value) => {
194
+ const rec = asRecord(value);
195
+ return [{ type: 'text', text: '容器 ' + rec.container + ' 已执行 ' + rec.action + '。' }];
196
+ },
197
+ },
198
+ async execute(rawArgs) {
199
+ const args = asRecord(rawArgs);
200
+ const container = assertContainerRef(requiredString(args, 'container', '容器名'));
201
+ const action = requiredString(args, 'action', '操作');
202
+ if (!['start', 'stop', 'restart', 'rm'].includes(action))
203
+ throw new Error('action 必须是 start / stop / restart / rm 之一(当前:' + action + ')。');
204
+ const result = await runChecked(runner, manageArgs(cfg.dockerPath, action, container), timeout, 'docker ' + action);
205
+ return { container, action, output: result.stdout.trim() };
206
+ },
207
+ timeoutMs: timeout,
208
+ };
209
+ return [dockerPs, dockerLogs, dockerInspect, dockerExec, dockerManage];
210
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@stardustlc/dsh-docker",
3
+ "version": "0.1.0",
4
+ "description": "DSH 容器管理插件:docker_ps/logs/inspect/exec/manage 五工具,走官方 subprocess 服务跑 docker CLI、argv 无 shell 注入、exec 审批门、零运行时依赖。",
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
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepare": "tsc -p tsconfig.json",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "keywords": [
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "plugin",
38
+ "docker",
39
+ "container",
40
+ "devops"
41
+ ],
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^24.0.0",
48
+ "typescript": "^5.6.0"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/STARDUSTLC666/dsh-docker.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/STARDUSTLC666/dsh-docker/issues"
56
+ },
57
+ "homepage": "https://github.com/STARDUSTLC666/dsh-docker#readme",
58
+ "author": "stardustlc",
59
+ "packageManager": "pnpm@11.7.0"
60
+ }