@hyzyn/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/LICENSE ADDED
@@ -0,0 +1,214 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+ Copyright 2026 dsh-plugin-kit contributors
203
+
204
+ Licensed under the Apache License, Version 2.0 (the "License");
205
+ you may not use this file except in compliance with the License.
206
+ You may obtain a copy of the License at
207
+
208
+ http://www.apache.org/licenses/LICENSE-2.0
209
+
210
+ Unless required by applicable law or agreed to in writing, software
211
+ distributed under the License is distributed on an "AS IS" BASIS,
212
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
213
+ See the License for the specific language governing permissions and
214
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,368 @@
1
+ # @hyzyn/dsh-docker
2
+
3
+ DSH Web GUI 的 **Docker 容器面板**插件:侧边栏「容器」入口打开面板,查看
4
+ **本机或 SSH 主机**上的容器列表 / 状态 / 端口 / 日志 / 资源占用与镜像,并在
5
+ 显式打开开关后执行启停删与一次性 `docker exec`。宿主半体用 `ssh2` 的
6
+ **exec channel**(非 PTY)在远程跑 docker CLI,本机目标直接 spawn;agent 侧
7
+ 配套 `docker_*` 工具,**默认只读**。
8
+
9
+ ## 与 dsh-tty 的关系(方案 A:独立插件,tty 零改动)
10
+
11
+ | 维度 | 说明 |
12
+ | --- | --- |
13
+ | 插件形态 | 独立包 `@hyzyn/dsh-docker`,不 import 任何 tty 代码,tty 也无需改一行源码;两者可各自单独安装、各自升级 |
14
+ | 连接簿 | SSH 目标可**引用 tty 连接簿条目名**(只读 `ctx.settings.get('tty')` 的 `sshHosts`);tty 未安装时退化为「内联 host/username」或本机目标 |
15
+ | 主机指纹 | 本插件自持一份 `hostKeys`(TOFU),并**优先以 tty 已记录的指纹作种子**——同一主机不必在两处各确认一次 |
16
+ | 执行通道 | 自持池化 SSH exec(`src/ssh-exec.ts`),与 tty 的 PTY 会话完全独立,互不占名额 |
17
+ | 上下文入口 | tty ≥ 0.13.0 时可选消费其客户端服务 `ttyConnbar`,在 SSH 连接栏(SFTP 旁)插入「容器」按钮(**注册即显示**),目标在点击时按当前会话解析;tty 未装 / 版本过旧则静默跳过 |
18
+ | 终端承载 | 交互式终端由 tty 承载(它才是 PTY 的所有者):tty ≥ 0.15 时经 `ttyTerminal.mount` **就地嵌入**到本面板底部的终端抽屉,tty ≥ 0.14 时退回「开标签 + 收面板」,都没有则复制命令。本插件不实现 PTY / xterm / 重连栈 |
19
+ | 分工 | **交互式排障**(`docker exec -it`、容器内 shell、TUI)由 tty 承载(抽屉内嵌或标签);**只读巡检与 agent 自动化**用本插件自己的 exec 通道 |
20
+
21
+ 数据级复用、代码级不耦合:连接簿与指纹种子是「读同一份 settings」,连接栏按钮是
22
+ 「消费一个通用扩展点」,都不是「依赖 tty 的模块」,因此 tty 升级或卸载都不会连带
23
+ 弄坏本插件。
24
+
25
+ ## 安装
26
+
27
+ ```bash
28
+ dsh plugin --profile web add @hyzyn/dsh-docker # npm 安装(发布后)
29
+ dsh plugin --profile web add link:$(pwd)/packages/docker # 仓库开发调试
30
+ ```
31
+
32
+ 聚合包 `@hyzyn/dsh-all`(或仓库根 bundle)已包含本插件,一次装齐时不必单独
33
+ add。装完重启 `dsh web`,侧边栏出现「容器」入口;设置 → 插件 →
34
+ 「Docker 容器面板」卡片维护目标与开关,**保存即热生效**(`settings/updated`
35
+ 触发重解析,无需重启)。
36
+
37
+ > 已在 web profile 里装过 `@hyzyn/dsh-all` 或根 bundle 时**不要**再 add 本包,
38
+ > 否则插件行重复挂载,启动报 `duplicate loader entry id`。
39
+
40
+ ## 使用
41
+
42
+ 两个入口,同一个面板:
43
+
44
+ - **侧边栏「容器」**(总入口):任何目标都能用,包括本机 docker 与多目标切换。
45
+ - **SSH 连接栏「容器」按钮**(上下文快捷方式,tty ≥ 0.13.0):在 tty 终端面板的
46
+ SSH 标签里,连接栏 SFTP 按钮旁会出现「容器」——**注册即显示**,点击直接用
47
+ **当前会话那台主机**打开面板,不用再选目标。目标解析发生在点击时:会话来自
48
+ 连接簿时按条目名匹配,否则按 `host:port` 匹配已解析的目标;**没配到目标也不会
49
+ 藏按钮**——面板会带一条提示告诉你会话主机(含连接簿名)该去设置卡片怎么配。
50
+
51
+ 面板内:
52
+
53
+ - **目标选择**:面板先选目标(来自配置 `targets`,本机 / SSH);只配置了一个
54
+ 目标时默认选中它,agent 工具也可以省略 `target` 参数。
55
+ - **容器列表**:名称 / 状态 / 健康态 / 镜像 / 端口映射 / compose 项目与服务 /
56
+ 短 ID;支持按名称或镜像**搜索**、按状态**筛选**(运行中 / 已停止 / 全部)。
57
+ - **容器卡片**:与参考布局一致的「标签 + 值」行(镜像 / ID / 端口 / 创建 /
58
+ compose,值等宽、可省略)+ 一排图标操作按钮,**按「查看 / 变更」两组用竖线分隔**:
59
+
60
+ | 组 | 按钮 | 说明 |
61
+ | --- | --- | --- |
62
+ | 查看 / 进入 | 终端、日志、资源占用 | 不改容器状态,只读模式下也永远可用 |
63
+ | 变更 | 启停、重启、删除 | 按破坏性递增排列;只在 `allowMutations` 开启后可用,未开启时整组置灰 |
64
+
65
+ 删除额外做了两点防护:破坏性配色 + 与「重启」之间留出间距,点击后仍需二次确认。
66
+ 点卡片本体进入概览。
67
+ - **容器详情(整栏视图)**:顶部为「返回 + 容器名 + 状态徽标 + 目标主机」,
68
+ 下方三个标签页——概览(`docker inspect` 权威数据 + 一次性 exec)、日志、
69
+ 统计。从卡片的日志 / 统计图标可直接落到对应标签页。
70
+ - **日志视图(紧凑两行)**:第一行 = 返回 + 容器名 + 状态 + 目标主机 +
71
+ `LINES`(尾部行数)/ `TIMESTAMPS` / `AUTO REFRESH`(开关 + 2/3/5/10s 间隔,
72
+ 仅日志页轮询)+ 刷新 / 下载 / 关闭;第二行 = 标签页 + 可折叠的「过滤日志」
73
+ (带匹配行数统计)。日志正文按级别着色(`[INFO]` 与 `|INFO` 两种常见前缀
74
+ 都能识别),时间戳压暗,过滤命中高亮;超过 2000 行只对尾部着色并提示。
75
+ 详情视图下不再叠加列表工具条与面板头,每屏只有一个刷新入口。
76
+ - **概览**:`docker inspect` 的权威数据——状态与健康、退出码、重启次数与策略、
77
+ 端口映射、挂载(含只读标记)、网络与 IP、entrypoint 与命令、最近一次健康
78
+ 检查输出;下方可执行一次性 `docker exec`(需 `allowExec`)。
79
+ - **日志**:`docker logs --tail` 的尾部快照(默认 `logTailDefault` 行),可切
80
+ 时间戳与 `--since`;输出超过 `maxOutputKb` 会截断并标记。
81
+ - **统计**:`docker stats --no-stream` 快照(CPU% / 内存用量与占比 / 网络 IO /
82
+ 块 IO / PIDs),面板按 `pollIntervalSec` 轮询刷新。
83
+ - **镜像**:`docker images` 列表(reference / 大小 / 创建时间 / 短 ID);
84
+ `<none>:<none>` 的 dangling 镜像带 `dangling` 标记。**只有列表,没有删除 /
85
+ 拉取 / 构建**。
86
+ - **一次性 exec**:`allowExec` 开启后可输入命令,等价
87
+ `docker exec <容器> sh -c "<命令>"`,返回退出码与 stdout/stderr(无 TTY)。
88
+ - **交互式终端(卡片第一个图标)**:跑 `docker exec -it '<容器>' sh`。按 tty 能力
89
+ 三级降级——
90
+ 1. **就地嵌入(tty ≥ 0.15,推荐)**:在面板底部开一个**终端抽屉**,由 tty 的
91
+ `ttyTerminal.mount` 把终端挂进来。面板不收起,看着容器日志直接进容器敲命令,
92
+ 上下文不断;收起抽屉即结束会话。
93
+ 2. **借 tty 弹窗开标签(tty ≥ 0.14)**:tty 新开一个标签执行同一命令,随后收起本
94
+ 面板(本面板 z-index 更高,不收起用户只会觉得「点了没反应」)。
95
+ 3. **复制命令**:未装 tty / 版本过旧 / 内联目标用了 key·password 认证(浏览器端
96
+ 没有凭证)时,退化为复制该命令并提示到终端面板粘贴。
97
+
98
+ 本地目标开本地会话,SSH 目标按连接簿条目名(或 agent 认证的内联字段)走 SSH,
99
+ 标签/抽屉标题为 `<容器> · exec`。能力判断走服务契约版本
100
+ (`ttyTerminal.version >= 2` 才有 `mount`),不是猜函数存不存在。
101
+
102
+ ### 目标(本机 / SSH)
103
+
104
+ | `kind` | 说明 |
105
+ | --- | --- |
106
+ | `local` | 宿主所在机器上的 docker CLI(`spawn` 直接执行,不经 shell) |
107
+ | `ssh` | 经 `ssh2` 连到远程主机,在远程执行 docker CLI(argv 经单引号转义) |
108
+
109
+ `kind=ssh` 有两种填法:
110
+
111
+ 1. **引用 tty 连接簿条目**:`book` 填条目名(在 设置 → 插件 → 终端面板 的
112
+ 连接簿里维护),主机 / 端口 / 用户名 / 认证方式随之生效。设置卡片的下拉
113
+ 只列出 tty 已保存的连接簿条目;tty 未安装或条目不存在时,该目标解析失败,
114
+ 面板与 agent 工具都会给出明确错误。
115
+ 2. **内联字段**:`host` + `username` 必填,其余按需(`port` / `auth` /
116
+ `keyPath` / `password` / `passphrase` / `agentForward`)。
117
+
118
+ 目标解析是**每次操作现算**的:在 tty 卡片里改了连接簿条目(换端口、改密码),
119
+ 下一次操作立即用新值,无需重启。远端需要满足:装了 docker CLI,且当前账号
120
+ **免 sudo** 可用 docker(通常在 `docker` 组),否则 `probe` 会原样透出
121
+ `permission denied while trying to connect to the Docker daemon socket`
122
+ 之类的错误。
123
+
124
+ ## 配置(设置 → 插件 → Docker 容器面板,保存即热生效)
125
+
126
+ 配置落在 settings 命名空间 `docker`,即 `~/.dsh/settings.yaml` 的 `docker:`
127
+ 段(`$DSH_HOME/settings.yaml`;DSH 的 settings 文件由宿主 `dsh-settings-file`
128
+ 提供)。插件行里的 composition 配置作为 schema `base` 打底,settings 层覆盖
129
+ 它;HTTP `POST /api/dsh-docker/config` 是卡片的写入通道,只接受下表这些键
130
+ (未知键返回 400)。
131
+
132
+ | 项 | 默认 | 说明 |
133
+ | --- | --- | --- |
134
+ | `enabled` | true | 关闭整个插件(**需重启 `dsh web` 生效**,与 tty 同语义) |
135
+ | `announceToAgent` | true | 是否向 agent 注入能力公告(systemPrompt section `plugin:dsh-docker`) |
136
+ | `dockerBin` | `docker` | docker CLI 可执行名或路径(podman 可填 `podman`);只允许字母、数字与 `_ . / -` |
137
+ | `allowMutations` | false | 允许 start / stop / restart / remove(面板按钮与 `docker_action` 工具;关闭时 `/action` 返回 403) |
138
+ | `allowExec` | false | 允许一次性 `docker exec`(面板 exec 输入与 `docker_exec` 工具;关闭时 `/exec` 返回 403) |
139
+ | `execTimeoutSec` | 30 | exec 默认超时秒数(1~120) |
140
+ | `pollIntervalSec` | 5 | 面板统计刷新间隔秒数(1~60) |
141
+ | `logTailDefault` | 200 | 日志默认尾部行数(1~5000) |
142
+ | `maxOutputKb` | 512 | 单次命令输出上限(KB,1~8192);超出截断并标记 `truncated` |
143
+ | `targets` | `[]` | 目标列表,见下 |
144
+ | `hostKeys` | `[]` | SSH 主机指纹记录(TOFU,自动维护) |
145
+
146
+ 数值越界会被夹到边界内,类型不符则回落到默认值。
147
+
148
+ ### `targets[]` 字段
149
+
150
+ | 字段 | 默认 | 说明 |
151
+ | --- | --- | --- |
152
+ | `name` | —(必填) | 目标展示名,唯一;空名或重名条目在保存时被丢弃(≤64 字符) |
153
+ | `kind` | `local` | `local` 本机 / `ssh` 远程 |
154
+ | `book` | `''` | `kind=ssh` 时引用 tty 连接簿条目名(留空则用下面的内联字段) |
155
+ | `host` | `''` | 内联主机名或 IP(无 `book` 时必填) |
156
+ | `port` | 22 | SSH 端口(1~65535,越界回落 22) |
157
+ | `username` | `''` | 内联 SSH 用户名(无 `book` 时必填) |
158
+ | `auth` | `agent` | `agent`(走 `SSH_AUTH_SOCK`)/ `key`(用 `keyPath`)/ `password`(用 `password`,同时挂 keyboard-interactive) |
159
+ | `keyPath` | `''` | `auth=key` 的私钥路径(`~` 开头会展开为 home) |
160
+ | `password` | `''` | `auth=password` 的密码;**建议填 `env:VAR`** 引用环境变量 |
161
+ | `passphrase` | `''` | 私钥口令;**建议填 `env:VAR`** 引用环境变量 |
162
+ | `agentForward` | false | 是否转发本机 ssh-agent(`SSH_AUTH_SOCK` 存在时生效) |
163
+
164
+ `password` / `passphrase` 里的 `env:VAR` 在连接时才解析(`process.env[VAR]`),
165
+ 变量缺失或为空会明确报 `环境变量未设置: VAR`。这两个值**永不回传浏览器**:
166
+ 配置快照里只给 `passwordSet` / `passphraseSet` 两个布尔位。
167
+
168
+ ### `hostKeys[]`(SSH 主机指纹,TOFU)
169
+
170
+ | 字段 | 说明 |
171
+ | --- | --- |
172
+ | `host` | 主机名或 IP(必填) |
173
+ | `port` | 端口,默认 22(按 host:port 唯一) |
174
+ | `fingerprint` | `hostHash: 'sha256'` 回调收到的原样十六进制指纹(必填) |
175
+
176
+ 首次连接自动记录并落盘;之后每次连接必须匹配,**指纹变更直接拒绝连接**,
177
+ 错误信息带「删除该主机记录再重连」的指引。记录列表在设置卡片里可删除重置。
178
+
179
+ ## agent 工具
180
+
181
+ | 工具 | 注册条件 | 参数 | 作用 / 典型用法 |
182
+ | --- | --- | --- | --- |
183
+ | `docker_targets` | 恒注册 | `probe?: boolean` | 列出目标(name / kind / label);`probe:true` 逐个探测 docker 版本与 daemon 可达性(SSH 目标会建连接,较慢)。其他工具的 `target` 取自这里 |
184
+ | `docker_ps` | 恒注册 | `target?`、`all?: boolean` | 列容器(名称 / 状态 / 健康 / 镜像 / 端口 / compose 项目与服务 / 短 ID);默认只列运行中,`all:true` 含已停止。排障第一步 |
185
+ | `docker_inspect` | 恒注册 | `target?`、`id`(必填) | `docker inspect` 的权威详情:状态 / 健康检查 / 退出码 / 重启次数 / 端口 / 挂载 / 网络 / 启动命令 |
186
+ | `docker_logs` | 恒注册 | `target?`、`id`、`tail?`(1~5000,默认 `logTailDefault`)、`timestamps?`、`since?` | `docker logs --tail` 尾部;`since` 用 docker 语法(如 `10m`、`2026-09-09T10:00:00`);超上限标记 `truncated` |
187
+ | `docker_stats` | 恒注册 | `target?`、`ids?`(逗号分隔的容器名/ID) | `docker stats --no-stream` 快照:CPU% / 内存用量与占比 / 网络 IO / 块 IO / PIDs;`ids` 省略 = 全部运行中容器 |
188
+ | `docker_images` | 恒注册 | `target?` | 镜像列表(仓库:标签 / 大小 / 创建时间 / 短 ID) |
189
+ | `docker_action` | 仅 `allowMutations` | `target?`、`action`(`start` \| `stop` \| `restart` \| `remove`)、`id` | 容器生命周期操作。`remove` 是破坏性的:删除容器配置与可写层(数据卷不在其中),执行前必须向用户确认目标容器 |
190
+ | `docker_exec` | 仅 `allowExec` | `target?`、`id`、`command`(必填,经容器内 `sh -c` 执行)、`timeoutSec?`(1~120,默认 `execTimeoutSec`) | 一次性 `docker exec`,返回退出码 / stdout / stderr;无 TTY,交互式排障请让用户到 tty 面板跑 `docker exec -it <容器> sh` |
191
+
192
+ - `target` 省略时回落到**唯一**已配置目标;配置了多个目标则必填,错误信息会
193
+ 列出可用目标名。
194
+ - 两个开关变化会**立即重注册**工具:关掉 `allowMutations` / `allowExec` 后,
195
+ 对应工具从 agent 侧消失,无需重启。
196
+ - 推荐排障顺序:`docker_targets` → `docker_ps` → `docker_logs` →
197
+ `docker_inspect` → `docker_stats`。
198
+ - `announceToAgent` 开启时,插件向 systemPrompt 注入一段能力公告(含「默认
199
+ 只读」「docker socket ≈ 目标主机 root」的约束提醒),让模型先列目标再动手。
200
+
201
+ ## HTTP 路由(`/api/dsh-docker` 前缀,全部 loopback 围栏)
202
+
203
+ 围栏校验 `remoteAddress`(127.0.0.1 / ::1 / ::ffff:127.0.0.1)、`Host`、
204
+ `Origin` 与 `sec-fetch-site`;非本机请求一律 403 `forbidden: loopback-only`。
205
+ 请求体上限 1MB,响应统一 `application/json` + `referrer-policy: no-referrer`。
206
+
207
+ | 路由 | 方法 | 请求体 | 返回 |
208
+ | --- | --- | --- | --- |
209
+ | `/config` | GET | — | `{ok:true, config}`:配置快照(targets 只给 `passwordSet` / `passphraseSet`,另附只读的 `ttyBooks` / `ttyAvailable` / `toolsRegistered`) |
210
+ | `/config` | POST | 上表配置键的任意子集 | `{ok:true, config}`;未知键 400,非法 JSON 400 |
211
+ | `/targets` | GET / POST | — | `{ok:true, targets:[{name, kind, label?\|error?}]}` |
212
+ | `/probe` | POST | `{target?}` | `{ok:true, probe:{ok, bin, serverVersion, error, target}}` |
213
+ | `/containers` | POST | `{target?, all?}` | `{ok:true, containers: ContainerSummary[]}` |
214
+ | `/inspect` | POST | `{target?, id}` | `{ok:true, details: ContainerDetail[]}` |
215
+ | `/stats` | POST | `{target?, ids?: string[]}` | `{ok:true, stats: ContainerStats[]}` |
216
+ | `/logs` | POST | `{target?, id, tail?, timestamps?, since?}` | `{ok:true, logs:{id, text, truncated}}` |
217
+ | `/images` | POST | `{target?}` | `{ok:true, images: ImageSummary[]}` |
218
+ | `/action` | POST | `{target?, action, id}` | 需 `allowMutations`(否则 403);`{ok:true, result:{id, action, message}}` |
219
+ | `/exec` | POST | `{target?, id, command, timeoutSec?}` | 需 `allowExec`(否则 403);`{ok:true, result:{id, command, code, stdout, stderr, truncated, durationMs}}` |
220
+
221
+ 其余子路径 404(`unknown route: ...`);`/config` 与 `/targets` 之外的 GET
222
+ 返回 405;执行失败(docker 报错、目标解析失败等)返回 500 或 400 加
223
+ `{error}` 文本。
224
+
225
+ ## 安全模型
226
+
227
+ **docker socket ≈ 目标主机的 root 权限。** 能访问 daemon 就能挂载宿主目录、
228
+ 以特权模式起容器、读容器里的密钥——因此本插件按「只读优先」设计:
229
+
230
+ 1. **默认只读**。`allowMutations` 未开启时,`/action` 返回 403,面板的启停删
231
+ 不可用,`docker_action` 工具**根本不注册**;`allowExec` 未开启时,`/exec`
232
+ 返回 403,`docker_exec` 工具同样不注册。两个开关互相独立,必须在设置卡片
233
+ 由用户显式打开。
234
+ 2. **破坏性操作要复述后果**。`remove` 映射为 `docker rm`(**不带 `-f`**),
235
+ agent 公告要求执行前向用户确认目标容器;运行中容器会报错并附
236
+ 「容器仍在运行:先停止再删除」的提示,不会静默强删。
237
+ 3. **凭证不落明文(建议)**。`password` / `passphrase` 支持 `env:VAR` 引用,
238
+ 配合 dsh-env-manager 托管密钥可避免明文写进 `settings.yaml`;`agent`
239
+ 认证(`SSH_AUTH_SOCK`)则完全不落盘。配置快照只回「是否已设置」。
240
+ 4. **主机指纹 TOFU 钉扎**。首次连接记录 sha256 指纹,之后必须一致,变更即
241
+ 拒绝连接(防中间人);tty 已确认过的主机会被当作种子直接信任并复制进
242
+ 本插件的记录。TOFU 的固有边界是「首次若已遭遇 MITM,记下的就是伪指纹」,
243
+ 以及按 host:port 只存一条(同主机多密钥类型可能误报变更,删除记录重连
244
+ 即可重新校准)。
245
+ 5. **命令一律 argv 构造,绝不做字符串拼接**。容器名 / ID 先过白名单
246
+ `assertRef`(`[A-Za-z0-9][A-Za-z0-9_.-]*`,≤128 字符,拒绝空格、`;`、
247
+ `$()`、反引号等),镜像与容器引用同理;远程经 `shJoin` 逐参数单引号
248
+ 转义后交给远端 shell,本机 `spawn(bin, args)` 不经 shell。
249
+ 6. **输出有上限**。`maxOutputKb` 限制单次命令的 stdout/stderr 字节数,超出
250
+ 截断并标记,避免大日志撑爆内存或 agent 上下文。
251
+ 7. **HTTP 只对本机开放**。全部路由走 loopback 围栏,远程浏览器无法调用。
252
+
253
+ ## 已知限制
254
+
255
+ - **没有交互式 TTY**:`exec` 是一次性命令(`docker exec <id> sh -c <cmd>`,
256
+ 不带 `-i` / `-t`),不能跑 vim / top / 交互式 shell,也不能喂 stdin 做
257
+ 对话。交互排障请到 tty 面板执行 `docker exec -it <容器> sh`(本机与 SSH
258
+ 目标都可以)。
259
+ - **没有实时日志流**:日志是 `--tail` 快照,要看新内容需重新拉取;`stats`
260
+ 是 `--no-stream` 单次快照,面板靠 `pollIntervalSec` 轮询。
261
+ - **docker CLI 版本差异**:解析走 `--format '{{json .}}'`,字段随版本增减,
262
+ 解析器一律降级而不抛异常(例如 `State` 缺失就从 `Status` 推导状态,健康态
263
+ 从 `(healthy)` / `(unhealthy)` 提取);缺字段时对应列可能为空,需要权威
264
+ 数据请用详情(`docker inspect`)。
265
+ - **`docker rm` 不带 `-f`**:运行中的容器删除会失败,错误里附「先停止再删除」
266
+ 提示;要强制删除得去 tty 面板手动 `docker rm -f`。
267
+ - **SSH 目标需要免 sudo 的 docker**:账号不在 docker 组时 docker 报权限错误,
268
+ 面板与工具原样透出,不做自动 sudo 提权。
269
+ - **远端未安装 docker**:`probe` 失败(`command not found` / 退出码 127),
270
+ 面板显示错误;PATH 不一致时可把 `dockerBin` 填成绝对路径。
271
+ - **podman 兼容靠 `dockerBin`**:填 `podman` 即可跑,但 `stats` 与
272
+ `--format '{{json .}}'` 的字段和输出格式与 docker 有差异,只能依赖解析器
273
+ 的降级路径,未逐项验证。
274
+ - **没有镜像删除 / 拉取 / 构建**:镜像区是只读列表。
275
+ - **没有多目标聚合视图**:一次只对一个目标操作,切目标需在面板或工具参数里
276
+ 显式选择。
277
+ - **连接栏按钮需要一条匹配的目标才有数据**:按钮在 SSH 标签上一律显示,但若会话
278
+ 主机没有对应的 `kind=ssh` 目标(连接簿名或 `host:port` 都匹配不上),点开只会看到
279
+ 「尚未配置为 Docker 目标」的提示而不是容器列表;本地标签的连接栏本身隐藏。
280
+ 目标增删后最多 30 秒内刷新(设置卡片保存会立即刷新)。
281
+ - **`enabled: false` 需重启**:关闭插件不会卸载已注册的路由与工具,重启
282
+ `dsh web` 才彻底停用。
283
+ - **变更操作无独立审计日志**:只有 docker 自身的记录与宿主 `ctx.logger` 的
284
+ 常规输出。
285
+
286
+ ## 工作原理
287
+
288
+ ```
289
+ 浏览器半体 (client.js)
290
+ ├─ 侧边栏「容器」入口 → 面板:目标选择 / 容器列表(搜索 + 状态筛选)/
291
+ │ 容器卡片(动作条分两组:查看=终端/日志/统计 | 变更=启停/重启/删除)/ 镜像列表 / 一次性 exec
292
+ │ └─ 终端抽屉:tty ≥ 0.15 时经 ttyTerminal.mount 就地嵌入 tty 的终端
293
+ │ (面板不收起;关抽屉即 dispose,tty 那边结束会话并拆 DOM)
294
+ │ └─ fetch → /api/dsh-docker/*(loopback 围栏)
295
+ ├─ 可选消费 tty 的 ttyConnbar 服务 → SSH 连接栏「容器」按钮
296
+ │ (按 book 名 / host:port 匹配已配置目标,命中才出现)
297
+ └─ 可选消费 tty 的 ttyTerminal 服务 → 卡片「终端」按钮直接开 docker exec 标签
298
+ (tty 不可用 / 凭证不在浏览器时退回复制命令)
299
+
300
+ 宿主半体 (src/index.ts)
301
+ ├─ settings 命名空间 docker(~/.dsh/settings.yaml)
302
+ │ DOCKER_SETTINGS_SCHEMA → normalizeConfig(夹紧 / 白名单键)
303
+ ├─ 只读复用 tty settings 的 sshHosts(连接簿)与 hostKeys(指纹种子)
304
+ ├─ resolveTarget:local → runLocal;ssh → book 查连接簿或内联字段
305
+ ├─ 每目标一个 DockerApi(src/docker.ts)
306
+ │ ├─ argv 构造 + assertRef 白名单 + 输出上限(maxOutputKb)
307
+ │ └─ 解析容错:{{json .}} 逐行/数组、字段名大小写兼容、缺字段降级
308
+ ├─ RemoteExec(src/ssh-exec.ts)
309
+ │ ├─ 懒连接池:同 user@host:port 复用一条连接,空闲 120s 回收
310
+ │ │ (每 30s 扫一次,连接超时 20s,keepalive 10s)
311
+ │ ├─ 非 PTY exec channel:一命令一 channel,收完 stdout/stderr 即关
312
+ │ ├─ shJoin 单引号转义(远端 shell 解析);env:VAR 取密
313
+ │ └─ hostVerifier TOFU 钉扎(首次记录、变更拒绝)
314
+ ├─ runLocal:spawn(dockerBin, args)(不经 shell,本机目标)
315
+ └─ agent 工具:docker_targets / docker_ps / docker_inspect /
316
+ docker_logs / docker_stats / docker_images(恒注册)
317
+ + docker_action(allowMutations)/ docker_exec(allowExec)
318
+ ```
319
+
320
+ ## 开发与验收
321
+
322
+ ```bash
323
+ pnpm --filter @hyzyn/dsh-docker build # tsc → lib/(宿主半体)+ esbuild → client.js(浏览器半体)
324
+ pnpm --filter @hyzyn/dsh-docker typecheck
325
+ pnpm --filter @hyzyn/dsh-docker smoke # 三套离线回归,都不需要 docker daemon
326
+ ```
327
+
328
+ `scripts/smoke.mjs`(22 项,读取 `lib/` 构建产物)覆盖纯逻辑:ps 解析(字段映射 /
329
+ compose 标签 / 端口 / `State` 缺失推导 / 噪声行 / JSON 数组)、端口串解析与去重、
330
+ stats 解析(百分比 / 内存 / IO / PIDs)、size 与 percent 的异常输入、images 解析
331
+ (dangling)、inspect 解析(状态 / 健康 / 退出码 / 挂载 / 网络 / 端口 / 缺字段不抛
332
+ 异常)、`assertRef` 注入拒绝、`assertBin`、`shJoin` 转义、`DockerApi` 的 argv 构造
333
+ (ps / logs / action / exec / probe 成败)、`normalizeConfig` 默认值与夹紧、
334
+ `sanitizeTargets` / `sanitizeHostKeys`、`resolveTarget` 的四种路径、
335
+ `mergeTargetSecrets` 的凭证保留语义。
336
+
337
+ `scripts/route-smoke.mjs`(28 项)用**假 cordis ctx + 假 docker CLI 脚本**跑端到端:
338
+ 插件挂载(settings / 工具 / 路由 / 能力公告注册)、11 条路由的实际调用与返回、
339
+ `/config` 凭证脱敏、未知配置键 400、默认只读时 `/action` 与 `/exec` 403 且对应
340
+ 工具不注册、打开开关后(含 `settings/updated` 热更新路径)立即解锁、非 loopback
341
+ 403、容器名注入尝试被白名单拒绝、省略 `target` 的回落与多目标报错。
342
+
343
+ `scripts/client-smoke.mjs`(8 项)在 Node 里用最小 DOM / React 桩执行构建产物
344
+ `client.js`:验证注册 id 与 factory 形状、只 require 平台 seed 提供的模块
345
+ (`react` / `react/jsx-runtime` / `react-dom/client`)、`apply` 注册的 settings
346
+ 卡片 key 等于命名空间 `docker`、找不到宿主侧边栏时安静降级且卸载可重复调用,ttyConnbar 集成的四条路径(连接簿名命中 / host:port 命中 / 未配置主机不加按钮 / tty 未安装静默跳过),以及侧边栏折叠态(`data-sidebar-collapsed`)隐藏入口标签的样式规则。
347
+ 需要真 daemon 的验证走下面的手工清单。
348
+
349
+ ### 手工验收清单
350
+
351
+ 1. **本机目标**:加一条 `kind=local` 的 `本机`,`probe` 返回 server 版本;
352
+ 容器列表与 `docker ps -a` 一致(含已停止容器)。
353
+ 2. **SSH 目标**:tty 连接簿里已有条目时,用 `book` 引用它 → 容器列表 / 详情 /
354
+ 日志正常;首次连接日志里出现「已记录 host key 指纹(TOFU)」,第二次不再
355
+ 提示;手动改掉 `hostKeys` 里的指纹后重连,应**被拒绝**并给出重置指引。
356
+ 3. **只读拦截**:两个开关都关时,`/action` 与 `/exec` 返回 403,agent 侧看不到
357
+ `docker_action` / `docker_exec`,面板对应按钮不可用。
358
+ 4. **日志 / 统计 / 镜像**:`tail` 与 `timestamps` / `since` 生效;统计显示
359
+ CPU、内存、网络与块 IO;镜像列表含 dangling 条目标记。
360
+ 5. **`allowMutations` 打开后**:stop / start / restart 成功;对运行中容器
361
+ remove 报错并附「先停止再删除」提示,先 stop 再 remove 成功。
362
+ 6. **`allowExec` 打开后**:`ls -la /app` 之类命令返回 stdout 与退出码;把命令
363
+ 换成 `sleep 60`(`timeoutSec` 调小)应被中断并报超时;`command` 超过 8000
364
+ 字符被拒绝。
365
+
366
+ ## 版本 / 许可证
367
+
368
+ `@hyzyn/dsh-docker` 0.1.0 · [Apache License 2.0](../../LICENSE)