dsh-plugin-container 0.0.0-stage → 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -2
  3. package/cordis.patch.yml +3 -0
  4. package/docs/ai-chat.png +0 -0
  5. package/docs/ai-new-session.png +0 -0
  6. package/docs/apple-container.png +0 -0
  7. package/docs/attach.png +0 -0
  8. package/docs/docker-console.png +0 -0
  9. package/docs/docker-overview.png +0 -0
  10. package/docs/docker-volumes.png +0 -0
  11. package/docs/image-architecture.png +0 -0
  12. package/docs/package-install.png +0 -0
  13. package/docs/provider-switch.png +0 -0
  14. package/docs/run-container.png +0 -0
  15. package/index.js +138 -0
  16. package/lib/client.js +3499 -0
  17. package/package.json +62 -4
  18. package/src/client/api.js +94 -0
  19. package/src/client/attach.jsx +133 -0
  20. package/src/client/components.jsx +301 -0
  21. package/src/client/hooks.js +161 -0
  22. package/src/client/i18n.js +496 -0
  23. package/src/client/icons.jsx +219 -0
  24. package/src/client/index.jsx +76 -0
  25. package/src/client/page.jsx +755 -0
  26. package/src/client/run-container.jsx +187 -0
  27. package/src/client/runs.js +182 -0
  28. package/src/client/store.js +91 -0
  29. package/src/client/styles.js +363 -0
  30. package/src/client/tabs/compose.jsx +94 -0
  31. package/src/client/tabs/console.jsx +211 -0
  32. package/src/client/tabs/containers.jsx +143 -0
  33. package/src/client/tabs/images.jsx +127 -0
  34. package/src/client/tabs/networks.jsx +91 -0
  35. package/src/client/tabs/overview.jsx +266 -0
  36. package/src/client/tabs/volumes.jsx +76 -0
  37. package/src/host/apple-cli.js +307 -0
  38. package/src/host/apple.js +471 -0
  39. package/src/host/attach.js +243 -0
  40. package/src/host/commands.js +235 -0
  41. package/src/host/docker-cli.js +405 -0
  42. package/src/host/jobs.js +245 -0
  43. package/src/host/parsers.js +484 -0
  44. package/src/host/runtime.js +285 -0
  45. package/src/host/service.js +832 -0
  46. package/src/host/tools.js +198 -0
  47. package/src/host/util.js +97 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-plugin-container 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,342 @@
1
- # Temporary Holding Version
1
+ # DSH 容器左边栏插件(Docker + Apple container)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 在 DeepSeek Harness 左侧栏加入 **容器** 工作台,面板右上角始终显示 **Docker** / **Apple container** 两个 provider 按钮,点击即可切换:
4
+
5
+ - **Docker**:macOS 上的 Docker CLI / context,兼容 Docker Desktop、colima、OrbStack、Rancher Desktop、Podman 等提供 Docker context 或 socket 的运行时。
6
+ - **Apple container**:macOS 26 / Apple silicon 原生 `container` CLI(Containerization 框架)。它不是 Docker:没有 Docker daemon/socket/context,每个容器一个轻量 VM,镜像/卷/网络独立,不支持 Compose。
7
+
8
+ ![Docker 概览](docs/docker-overview.png)
9
+
10
+ ![Provider switch](docs/provider-switch.png)
11
+
12
+ ![Apple container 概览](docs/apple-container.png)
13
+
14
+ 当前机器验证环境:
15
+
16
+ - macOS 26.5.1,arm64
17
+ - Docker CLI 28.2.2,colima context 正常
18
+ - Apple `container` CLI 1.0.0(`/opt/homebrew/bin/container`)
19
+ - Docker / Apple 两个 provider 均已通过 Playwright UI 冒烟
20
+ - Apple 系统服务是否运行以面板状态为准;未启动时点击“启动运行时”
21
+
22
+ ## 功能
23
+
24
+ ### Docker provider
25
+
26
+ - 自动识别 colima、Docker Desktop、OrbStack、远程 context。
27
+ - 概览、容器(搜索/筛选/启动/停止/重启/日志/Inspect/删除)、镜像(拉取/Inspect/删除/架构)、卷、网络、Compose(只读)。
28
+ - **镜像架构**:列表新增“架构”列;Docker 通过 `image inspect` 补齐,Apple 从 OCI variants 读取,显示如 `linux/arm64/v8`。
29
+ - **启动新容器**:镜像下拉选择本地镜像/自定义镜像,名称、命令、端口、环境变量,后台运行;启动后立即可 Attach。
30
+ - **从镜像启动**:镜像页每行的播放按钮会用该镜像预填并打开启动容器表单。
31
+ - **Attach 终端**:基于 DSH PTY 的 `docker attach`,可以输入命令、Ctrl+C、粘贴,关闭对话框结束会话。
32
+ - 运行时启停/重启:colima 完整支持;Docker Desktop / OrbStack best-effort。
33
+ - 内置命令终端:选择 context、工作目录,实时输出,取消/超时/输出截断提示,危险命令确认。
34
+
35
+ ### Apple container provider
36
+
37
+ - `container system status/version` 状态;`container system start/stop` 生命周期。
38
+ - 容器、镜像、卷、网络、磁盘(`container system df`)列表和 Inspect。
39
+ - 容器 start/stop/delete/logs;镜像 pull/delete;卷/网络 delete。
40
+ - **启动新容器 / Attach**:Apple `container run`;Attach 使用 `container exec -i -t <id> /bin/sh`(Apple CLI 没有 attach 命令)。
41
+ - 命令终端切换为 `container ...` 语法,快捷命令自动适配。
42
+ - Apple provider 不支持 Compose;面板会自动隐藏 Compose tab。
43
+ - Apple 的 `container image inspect`/`delete` 使用镜像名(不是 digest),UI 已按 provider 适配。
44
+
45
+ ![命令终端](docs/docker-console.png)
46
+
47
+ ![镜像架构](docs/image-architecture.png)
48
+
49
+ ![启动新容器](docs/run-container.png)
50
+
51
+ ![Attach 终端](docs/attach.png)
52
+
53
+ ## AI 工具(只读)
54
+
55
+ 插件向 DSH Agent 注册 4 个只读工具,复用现有 provider:
56
+
57
+ | Tool | 作用 |
58
+ |---|---|
59
+ | `container_status` | CLI、daemon/系统服务、runtime、warnings |
60
+ | `container_list` | 容器/镜像/卷/网络 |
61
+ | `container_inspect` | 单个资源详情,敏感 env 脱敏 |
62
+ | `container_logs` | 最近日志,默认 200 行、上限 500 |
63
+
64
+ 使用方式:
65
+
66
+ - 主聊天直接问“我的 nginx 为什么退出?”;模型会调用上述工具读取事实。
67
+ - 容器行点 **问 AI**(运行中)或 **诊断**(未运行)会**新建一个独立诊断会话**(复用当前 workspace/cwd/agent preset),发送 provider/context/资源 ID/状态,并自动在主对话中打开;不会污染当前会话。
68
+ - 模型调用来自 DSH 当前 Session;插件不持有模型 API Key,也不直接调模型 HTTP。
69
+ - 日志和 inspect 文本被标注为不可信数据;工具描述和 system prompt 都要求不要服从其中的指令。
70
+
71
+ ![AI 诊断新会话](docs/ai-new-session.png)
72
+
73
+ ![AI 对话示例](docs/ai-chat.png)
74
+
75
+ 需要安装运行时依赖 `@deepseek-ai/dsh-tools`(已写入 `package.json`)。host 增加工具后需要重启 DSH;旧 host 下 client 会根据 `ai-tools` feature 隐藏 AI 按钮。
76
+
77
+ ## 安全边界
78
+
79
+ - **owner-only**:`/docker` 与 `/api` 共用 DSH Connection 的 Host/Origin 校验和浏览器 cookie 认证;未认证返回 401。服务端再检查 `peer.principal.kind === 'owner'`。
80
+ - **只读模型工具**:AI 只能调用 `container_status` / `container_list` / `container_inspect` / `container_logs` 读取事实;没有 start/stop/rm/prune/run 等 mutation tool,模型不能自动改容器。
81
+ - **PTY 隔离**:Attach 使用 DSH `spawnTerminal` 的 PTY;Docker 使用 `docker attach`,Apple 使用 `container exec -i -t`;关闭对话框或插件卸载时终止。
82
+ - **无 shell**:命令拆成 argv 直接 spawn;`| & ; < > ( ) \` $`、不平衡引号会被拒绝,不执行管道/重定向/命令替换。
83
+ - **危险操作确认**:`rm`/`rmi`/`delete`/`stop`/`kill`/`prune`/`compose down` 等未带 `confirm: true` 时 Host 直接返回 `docker/confirmation-required`,不会 spawn;UI 显示实际命令。
84
+ - **上下文锁定**:Docker 命令自动加 `--context`;客户端传入 `--context`/`--host`/`-H`/`--config`/`--tls*` 会被拒绝。Apple provider 没有 context。
85
+ - **输出与历史**:输出只在 host 内存中保留(上限 1 MiB/stream),不写日志;命令历史存 localStorage,敏感命令(login/password/token)不记录。
86
+ - **权限**:命令以 DSH app 当前用户权限执行,等价于本机终端。`container system start` 可能触发管理员权限/launchd/kernel 安装;插件不会静默 sudo。
87
+
88
+ ## 安装与生效
89
+
90
+ 插件以 DSH bundle 包发布,安装时会自动写入 `dependencies` 和 `dsh.profile.bundles`,**不需要手改 profile 的 `cordis.patch.yml`**。
91
+
92
+ 本地 tarball:
93
+
94
+ ```bash
95
+ dsh plugin --profile desktop add /path/to/dsh-plugin-container-0.2.0.tgz
96
+ ```
97
+
98
+ 发布到 npm 后:
99
+
100
+ ```bash
101
+ dsh plugin --profile desktop add dsh-plugin-container
102
+ ```
103
+
104
+ 安装后 profile 会变成:
105
+
106
+ ```json
107
+ "dependencies": {
108
+ "dsh-plugin-container": "file:/path/to/dsh-plugin-container-0.2.0.tgz"
109
+ },
110
+ "dsh": {
111
+ "profile": {
112
+ "bundles": [
113
+ "@deepseek-ai/dsh-base",
114
+ "@deepseek-ai/dsh-web-app",
115
+ "dsh-plugin-container"
116
+ ]
117
+ }
118
+ }
119
+ ```
120
+
121
+ 包内的 `cordis.patch.yml` 会被 DSH 自动应用:
122
+
123
+ ```yaml
124
+ - insert:
125
+ - id: docker-sidebar
126
+ name: dsh-plugin-container
127
+ ```
128
+
129
+ 本机安装后的效果:
130
+
131
+ ![本地包安装](docs/package-install.png)
132
+
133
+ **host 侧代码更新后必须退出并重开 DSH 一次**;client bundle 改动由 `client-hmr` 热替换。bundle 变化需要重启 host。
134
+
135
+ 如果右上角只看到 Docker / Apple container 但没有“启动容器”和 Attach 按钮,说明当前 host 还是旧进程:client 会显示“重启 DSH 以启用启动容器 / Attach”,退出并重开 DSH 即可。
136
+
137
+ ### 源码开发安装
138
+
139
+ 仍可用绝对路径方式开发:
140
+
141
+ ```bash
142
+ node scripts/install-profile.mjs --home "$HOME/.dsh" --profile desktop
143
+ ```
144
+
145
+ 它会写入带标记的 Loader row,指向当前工作区的 `index.js`,适合改 host 代码时热重载。源码模式不要和 bundle 包同时启用,否则会加载两份插件。
146
+
147
+ ### 手动安装(源码模式)
148
+
149
+ 直接编辑 patch 文件:
150
+
151
+ ```yaml
152
+ # >>> dsh-plugin-container >>>
153
+ - insert:
154
+ - id: docker-sidebar
155
+ name: '/Users/zaynfang/Codes/dsh/docker/index.js'
156
+ # <<< dsh-plugin-container <<<
157
+ ```
158
+
159
+ ### 卸载
160
+
161
+ ```bash
162
+ dsh plugin --profile desktop remove dsh-plugin-container
163
+ ```
164
+
165
+ 再从 `dsh.profile.bundles` 中确认已移除;重启 DSH。源码方式则运行:
166
+
167
+ ```bash
168
+ node scripts/install-profile.mjs --remove --home "$HOME/.dsh" --profile desktop
169
+ ```
170
+
171
+ ## 配置
172
+
173
+ Loader row 可带可选 `config`:
174
+
175
+ ```yaml
176
+ - insert:
177
+ - id: docker-sidebar
178
+ name: '/Users/zaynfang/Codes/dsh/docker/index.js'
179
+ config:
180
+ dockerPath: /opt/homebrew/bin/docker # 显式配置后不回退自动探测
181
+ colimaPath: /opt/homebrew/bin/colima
182
+ containerPath: /opt/homebrew/bin/container # Apple container CLI
183
+ defaultTimeoutMs: 120000
184
+ maxOutputBytes: 1048576
185
+ maxConcurrentRuns: 4
186
+ enableRuntimeControl: true
187
+ ```
188
+
189
+ 不配置时:
190
+
191
+ - `docker` 依次探测 `/opt/homebrew/bin/docker`、`/usr/local/bin/docker`、`~/.docker/bin/docker`、Docker.app 内置路径,再走 PATH。
192
+ - `colima` 依次探测 `/opt/homebrew/bin/colima`、`/usr/local/bin/colima`、`~/go/bin/colima`,再走 PATH。
193
+ - `container` 依次探测 `/usr/local/bin/container`、`/opt/homebrew/bin/container`、`~/.local/bin/container`,再走 PATH。
194
+ - 运行时启停只在 macOS 且 `enableRuntimeControl !== false` 时可用。
195
+
196
+ ## macOS 运行时说明
197
+
198
+ Docker 兼容运行时包括 Docker Desktop、Colima、OrbStack、Rancher Desktop、Podman machine,以及 Lima/UTM/Parallels/VMware 里自建的 Docker;它们通常通过 Docker CLI/context 工作,因此由 **Docker provider** 统一管理(具体 context 由各运行时提供)。
199
+
200
+ Apple `container` 是独立平台:
201
+
202
+ - 仅 macOS 26 + Apple silicon。
203
+ - 每个容器一个轻量 VM,没有共享 Docker daemon。
204
+ - 安装:从 [apple/container releases](https://github.com/apple/container/releases) 安装,或使用 Homebrew;然后 `container system start`。
205
+ - `container system status` 在部分安装上会显示 `unregistered`,但 apiserver 实际可达;插件会用 `container ls` 做健康探测兜底。
206
+ - 系统服务启动可能需要管理员权限/launchd/kernel;插件使用 `--disable-kernel-install` 避免交互式挂起,失败时提示在终端执行 `container system start`。
207
+ - 不支持 Compose;镜像/卷/网络与 Docker 不共享;不支持一键 restart(需先 stop 再 start)。
208
+
209
+ ## 工作原理
210
+
211
+ ```text
212
+ 左栏 sidebar.panellist + main keyed panel "docker"
213
+ │
214
+ ▼
215
+ Browser ctx.connection.rpc.call('/docker', method, { provider, ... })
216
+ │
217
+ ▼
218
+ Host ctx.webServer prefix route /docker
219
+ ctx.connection.admit(req)
220
+ DockerService (router)
221
+ ├─ Docker provider → docker CLI + context + JobManager(idPrefix=docker)
222
+ └─ Apple provider → container CLI + JobManager(idPrefix=apple)
223
+ │
224
+ ▼
225
+ ctx.subprocess.spawn(argv) ← 无 shell,provider 决定 CLI 语法
226
+ ```
227
+
228
+ - `providers` 返回可用 provider 和默认 provider。
229
+ - `status/list/inspect/run/action/runtimeAction` 都接受 `provider`;不传默认 Docker。
230
+ - `poll/cancel` 按 run id 前缀(`docker_` / `apple_`)路由,切换 provider 时也能取消旧 run。
231
+ - 语义化 `action`(container start/stop/delete/logs、image pull/delete、volume/network delete)由 host 映射成对应 CLI,UI 不拼 Docker/Apple 语法。
232
+
233
+ ### RPC 方法
234
+
235
+ | method | 说明 |
236
+ |---|---|
237
+ | `providers` | 可用 provider、版本、运行状态 |
238
+ | `status` | CLI、daemon/system、context/runtime、资源计数 |
239
+ | `list` | containers / images / volumes / networks / compose(Docker) / disk |
240
+ | `inspect` | container / image / volume / network |
241
+ | `action` | 语义化快捷操作(含启动新容器) |
242
+ | `attach` | 启动 Docker attach / Apple exec PTY 会话 |
243
+ | `attachPoll` / `attachInput` / `attachResize` / `attachClose` | Attach 输出、输入、尺寸、关闭 |
244
+ | `run` | 启动自由命令作业 |
245
+ | `poll` / `cancel` | 增量输出 / 取消 |
246
+ | `runtimeAction` | Docker 运行时或 Apple system start/stop |
247
+
248
+ ## 发布到 npm
249
+
250
+ ```bash
251
+ npm login
252
+ npm publish --access public
253
+ ```
254
+
255
+ 发布后安装:
256
+
257
+ ```bash
258
+ dsh plugin --profile desktop add dsh-plugin-container
259
+ ```
260
+
261
+ 当前包声明了 `dsh.bundle.patch`,安装器会自动写入 `dependencies` 和 `dsh.profile.bundles`,不需要手改 `cordis.patch.yml`。
262
+
263
+ ## 开发
264
+
265
+ 构建期依赖只有 esbuild;`lib/client.js` 已提交,运行不需要 `npm install`。
266
+
267
+ 本机网络走 SOCKS5 8119(8119 不是 HTTP proxy):
268
+
269
+ ```bash
270
+ env -u http_proxy -u https_proxy -u ALL_PROXY -u all_proxy -u NODE_USE_ENV_PROXY \
271
+ ALL_PROXY=socks5h://127.0.0.1:8119 \
272
+ npm_config_proxy=socks5h://127.0.0.1:8119 \
273
+ npm_config_https_proxy=socks5h://127.0.0.1:8119 \
274
+ NO_PROXY=127.0.0.1,localhost,::1 \
275
+ npm install --no-audit --no-fund
276
+ ```
277
+
278
+ ```bash
279
+ npm run build # src/client/index.jsx -> lib/client.js
280
+ node --check lib/client.js
281
+ npm test # 45 个 node:test 单测
282
+ ```
283
+
284
+ 修改 client 后重新 build;`client-hmr` 会热替换。修改 `index.js` 或 `src/host/*` 后重启 DSH。
285
+
286
+ ### UI 冒烟
287
+
288
+ ```bash
289
+ PLAYWRIGHT_CHROMIUM_EXECUTABLE="/Users/zaynfang/Library/Caches/ms-playwright/chromium-1208/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" \
290
+ node scripts/ui-smoke.mjs "http://127.0.0.1:<port>/?token=..."
291
+ ```
292
+
293
+ 冒烟覆盖:
294
+
295
+ - Docker provider:状态、容器/镜像/卷/网络/Compose、`docker ps -a`、`docker events` 取消、危险命令确认、Inspect。
296
+ - 切换到 Apple provider:provider 选择器、Apple 运行状态、Compose 隐藏、`container ls -a`。
297
+ - 不删除资源、不停止运行时;截图写入 `docs/`。
298
+
299
+ ## 文件结构
300
+
301
+ ```text
302
+ index.js Host 入口 / /docker 认证路由
303
+ cordis.patch.yml bundle 安装时自动插入的 Loader row
304
+ lib/client.js 构建产物
305
+ src/host/service.js provider 路由 + Docker provider
306
+ src/host/apple.js Apple container provider
307
+ src/host/apple-cli.js container CLI 探测/执行
308
+ src/host/attach.js PTY attach 会话管理
309
+ src/host/tools.js 只读 Agent Tools
310
+ src/host/docker-cli.js Docker CLI
311
+ src/host/runtime.js colima/Docker Desktop/OrbStack/remote
312
+ src/host/jobs.js 命令作业(docker_/apple_ 前缀)
313
+ src/host/parsers.js Docker / Apple JSON 归一化
314
+ src/client/ provider 切换、页面、tabs、i18n、styles
315
+ src/client/attach.jsx Attach 终端模态
316
+ src/client/run-container.jsx 启动新容器表单
317
+ scripts/build-client.mjs
318
+ scripts/install-profile.mjs
319
+ scripts/ui-smoke.mjs
320
+ test/*.test.mjs
321
+ docs/*.png
322
+ ```
323
+
324
+ ## 限制
325
+
326
+ - Docker `attach` 需要容器有 TTY;“启动容器”表单默认使用 `-dit`,已有容器若创建时没有 TTY,`docker attach` 可能无法交互,请改用日志或重新创建容器。
327
+ - Apple container 没有 `attach` 命令;面板使用 `container exec -i -t <id> /bin/sh`,镜像内必须存在 `/bin/sh`。
328
+ - Attach 依赖 DSH 的 PTY(`spawnTerminal`);若部署环境禁用 PTY,会返回明确的 `attach-unsupported` 错误。
329
+ - Attach 是单终端文本视图,不包含完整 xterm 能力(鼠标、复杂 TUI、图片协议可能不完整)。
330
+ - 启动新容器固定后台运行(`-d`);交互式前台运行请使用命令终端。
331
+ - Apple provider 仍然不支持 Compose。
332
+ - host 新增 Attach/启动新容器后需要再重启 DSH 一次;旧 host 下 client 会显示“重启 DSH 以启用启动容器 / Attach”,不会出现无效按钮。
333
+
334
+ ## 故障排查
335
+
336
+ - **左栏没有“容器”**:确认 patch 块存在;刷新 GUI;host 代码更新后退出并重开 DSH。
337
+ - **只有 Docker,没有 Apple container 选项**:确认 `container` CLI 可执行(`container --version`);刷新;host 需要新版本。
338
+ - **Apple 服务显示未启动**:点击“启动运行时”,或终端执行 `container system start`。首次可能需要管理员权限/kernel。
339
+ - **Apple 启动卡住/失败**:查看命令终端里的 `container system start` 输出;`--disable-kernel-install` 下缺 kernel 会失败,需先在终端启动一次。
340
+ - **Apple 镜像 Inspect 失败**:请使用镜像名(UI 已自动这样做),不要用 digest。
341
+ - **Docker Desktop 停止失败**:macOS Automation 权限可能拦截 `osascript`;从菜单栏退出或授予权限。
342
+ - **patch 损坏**:用 `cordis.patch.yml.docker.bak` 恢复,或 `install-profile.mjs --remove` 后重试。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: docker-sidebar
3
+ name: dsh-plugin-container
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/index.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Host entry for the Docker sidebar plugin.
3
+ *
4
+ * This file is loaded by the DSH Loader from an absolute path, so it imports
5
+ * only local modules and Node built-ins. The browser half is served separately
6
+ * from `lib/client.js` through the package's `dsh.client` declaration.
7
+ *
8
+ * The browser calls the same `/docker/...` envelope used by the Connection
9
+ * generic RPC channel. We mount the route ourselves because Connection's
10
+ * `rpc.handle` helper registers on the service owner's context, which does not
11
+ * inject `webServer`; the direct route below reuses `connection.admit()` for
12
+ * the same Host/Origin fence and browser authentication.
13
+ */
14
+ import { createDockerService } from './src/host/service.js'
15
+ import { registerContainerTools } from './src/host/tools.js'
16
+
17
+ export const name = 'docker-sidebar'
18
+ export const inject = ['subprocess', 'connection', 'webServer', 'tools', 'systemPrompt']
19
+
20
+ const CHANNEL = '/docker'
21
+ const MAX_BODY_BYTES = 1024 * 1024
22
+
23
+ /** @param {import('node:http').IncomingMessage} req */
24
+ function readBody(req) {
25
+ return new Promise((resolve, reject) => {
26
+ const chunks = []
27
+ let size = 0
28
+ req.on('data', (chunk) => {
29
+ size += chunk.byteLength
30
+ if (size > MAX_BODY_BYTES) {
31
+ reject(new Error('request body too large'))
32
+ req.destroy()
33
+ return
34
+ }
35
+ chunks.push(chunk)
36
+ })
37
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
38
+ req.on('error', reject)
39
+ })
40
+ }
41
+
42
+ /** @param {import('node:http').ServerResponse} res @param {number} status @param {unknown} body */
43
+ function send(res, status, body, contentType = 'application/json; charset=utf-8') {
44
+ if (res.writableEnded) return
45
+ const text = typeof body === 'string' ? body : JSON.stringify(body)
46
+ res.writeHead(status, { 'content-type': contentType, 'cache-control': 'no-store' })
47
+ res.end(text)
48
+ }
49
+
50
+ /**
51
+ * @param {import('node:http').IncomingMessage} req
52
+ * @param {import('node:http').ServerResponse} res
53
+ * @param {any} ctx
54
+ * @param {ReturnType<typeof createDockerService>} service
55
+ */
56
+ async function handleRequest(req, res, ctx, service) {
57
+ let admission
58
+ try {
59
+ admission = ctx.connection.admit(req)
60
+ } catch (error) {
61
+ send(res, 403, `forbidden: ${error instanceof Error ? error.message : String(error)}`, 'text/plain; charset=utf-8')
62
+ return
63
+ }
64
+ if ('rejection' in admission) {
65
+ send(res, admission.rejection, admission.rejection === 401 ? 'unauthorized' : 'forbidden', 'text/plain; charset=utf-8')
66
+ return
67
+ }
68
+ if (req.method !== 'POST') {
69
+ send(res, 405, 'method not allowed', 'text/plain; charset=utf-8')
70
+ return
71
+ }
72
+ const url = new URL(req.url ?? CHANNEL, 'http://dsh.internal')
73
+ const endpoint = url.pathname.startsWith(`${CHANNEL}/`) ? url.pathname.slice(CHANNEL.length + 1) : ''
74
+ if (endpoint === '' || endpoint.includes('/')) {
75
+ send(res, 404, 'not found', 'text/plain; charset=utf-8')
76
+ return
77
+ }
78
+
79
+ let message
80
+ try {
81
+ message = JSON.parse(await readBody(req))
82
+ } catch (error) {
83
+ send(res, 400, `invalid request body: ${error instanceof Error ? error.message : String(error)}`, 'text/plain; charset=utf-8')
84
+ return
85
+ }
86
+ if (
87
+ message === null ||
88
+ typeof message !== 'object' ||
89
+ message.type !== 'client-request' ||
90
+ typeof message.rpcId !== 'string' ||
91
+ typeof message.method !== 'string' ||
92
+ message.method !== endpoint
93
+ ) {
94
+ send(res, 400, 'invalid client-request envelope', 'text/plain; charset=utf-8')
95
+ return
96
+ }
97
+
98
+ const controller = new AbortController()
99
+ const abort = () => controller.abort()
100
+ res.on('close', abort)
101
+ try {
102
+ const result = await service.handle(message.method, message.payload, controller.signal, admission.peer)
103
+ send(res, 200, { type: 'server-response', rpcId: message.rpcId, result })
104
+ } catch (error) {
105
+ send(res, 500, `handler failure: ${error instanceof Error ? error.message : String(error)}`, 'text/plain; charset=utf-8')
106
+ } finally {
107
+ res.off('close', abort)
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @param {import('@deepseek-ai/cordis').Context} ctx
113
+ * @param {Record<string, any>} [config]
114
+ */
115
+ export function apply(ctx, config = {}) {
116
+ const service = createDockerService({
117
+ subprocess: ctx.subprocess,
118
+ logger: ctx.logger,
119
+ config: config ?? {},
120
+ })
121
+ try {
122
+ registerContainerTools(ctx, service)
123
+ service.enableTools()
124
+ } catch (error) {
125
+ ctx.logger?.warn?.('docker: container tool registration failed', error)
126
+ }
127
+ ctx.effect(
128
+ () => ctx.webServer.register({
129
+ kind: 'prefix',
130
+ path: CHANNEL,
131
+ handler: (req, res) => handleRequest(req, res, ctx, service).catch((error) => {
132
+ send(res, 500, `handler failure: ${error instanceof Error ? error.message : String(error)}`, 'text/plain; charset=utf-8')
133
+ }),
134
+ }),
135
+ 'docker: rpc route',
136
+ )
137
+ ctx.effect(() => () => service.dispose(), 'docker: jobs')
138
+ }