dsh-server-connector 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.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +572 -2
- package/cordis.patch.yml +6 -0
- package/icon.svg +15 -0
- package/lib/audit.js +455 -0
- package/lib/client.js +1716 -0
- package/lib/index.js +266 -0
- package/lib/known-hosts.js +377 -0
- package/lib/pending.js +753 -0
- package/lib/policy.js +590 -0
- package/lib/pool.js +1464 -0
- package/lib/registry.js +852 -0
- package/lib/routes.js +1527 -0
- package/lib/tools.js +1357 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +105 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- **每台服务器独立的启用/禁用开关**。禁用后 `srv_list` 与系统提示都不再列出该服务器,
|
|
8
|
+
任何 `srv_*` 调用返回 `SERVER_DISABLED`;面板里该行置灰并标注「已禁用」,随时可恢复。
|
|
9
|
+
已连接的服务器必须先断开才能禁用。切换不走待确认区,写审计 `server-enabled` / `server-disabled`。
|
|
10
|
+
- **每台服务器独立的自定义高危规则**。在详情页给单台机器补规则(正则 + `confirm`/`block`),
|
|
11
|
+
与内置 20 条走同一套判定。保存时六道校验:长度、可编译、不匹配空串、拒绝嵌套量词(ReDoS)、
|
|
12
|
+
必须命中至少一条样例、id 不冲突。配套 **试跑** 接口,逐条报告样例是否命中且不写盘。
|
|
13
|
+
- `POST /server-connector/servers/:id/rules/preview` 等四条自定义规则路由。
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- **长任务读取报「belongs to another session」**。`srv_job_output` / `srv_job_kill` 把调用者
|
|
18
|
+
算成 `undefined` 而非本次调用的会话,导致本会话发起的后台任务对自己也不可见。
|
|
19
|
+
测试替身此前忽略归属参数,因此没有暴露该问题;现已补上同等的归属校验与回归测试。
|
|
20
|
+
- **Windows 上私钥认证无法提交**。表单与路由都要求私钥路径以 `/` 开头,
|
|
21
|
+
使 `C:\…` 与 UNC 路径一律被拒;现已接受盘符与 UNC 路径(`defaultWorkDir` 仍要求 POSIX,
|
|
22
|
+
因为它是远端路径)。
|
|
23
|
+
|
|
24
|
+
### 文档
|
|
25
|
+
|
|
26
|
+
- 设计规格从 `2026-11-04-server-connector-plugin-design.md` 改名为 `docs/design.md`,
|
|
27
|
+
并补入自定义规则(§4.4)与启用开关相关章节。
|
|
28
|
+
- README 重写安装章节,覆盖 npm、GitHub Release tarball、GitHub 源码、本地 checkout 四条渠道。
|
|
29
|
+
|
|
30
|
+
### 兼容性
|
|
31
|
+
|
|
32
|
+
- `servers.json` 向前兼容:缺少 `enabled` 字段的旧记录按**启用**加载,缺少 `customRules`
|
|
33
|
+
的按空列表加载。
|
|
34
|
+
- 无破坏性变更。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 0.1.0 — 2026-11-05
|
|
39
|
+
|
|
40
|
+
首个公开发布版本。
|
|
41
|
+
|
|
42
|
+
- 六个 `srv_*` 工具:`srv_list` / `srv_connect` / `srv_exec` / `srv_job_output` /
|
|
43
|
+
`srv_job_kill` / `srv_policy_check`。
|
|
44
|
+
- 内置 20 条高危命令规则(8 条 block、12 条 confirm),逐台可调。
|
|
45
|
+
- SSH 连接池、主机指纹校验、审计日志(JSONL)、两段式危险操作确认。
|
|
46
|
+
- 侧边栏「服务器」面板、服务器表单、设置页。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 niechao136
|
|
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,573 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-server-connector
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DSH(DeepSeek Harness)插件:在本机配置好可运维的 SSH 服务器后,让 Harness 里的 agent 用
|
|
4
|
+
`srv_*` 工具直接在那些机器上执行命令;内置高危命令策略、逐条审批、执行审计与两段式危险操作确认。
|
|
5
|
+
|
|
6
|
+
本 README 侧重**怎么用**:安装、添加服务器、让 agent 执行、看懂审批与审计、调整配置。
|
|
7
|
+
设计细节(数据模型、实现取舍)见 [`docs/design.md`](./docs/design.md)。
|
|
8
|
+
|
|
9
|
+
- 运行环境:DSH `>=0.1.0-rc.6 <0.3.0`,Node `^22.19.0 || >=24.0.0`
|
|
10
|
+
- 插件形态:Host 半(`srv_*` 工具、连接池、策略、审计)+ Client 半(侧边栏「服务器」面板与设置页)
|
|
11
|
+
- 数据目录:`$DSH_HOME/server-connector/`(`$DSH_HOME` 默认 `~/.dsh`)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 目录
|
|
16
|
+
|
|
17
|
+
- [它能做什么](#它能做什么)
|
|
18
|
+
- [安装](#安装)
|
|
19
|
+
- [五分钟上手](#五分钟上手)
|
|
20
|
+
- [服务器面板](#服务器面板)
|
|
21
|
+
- [Agent 工具](#agent-工具)
|
|
22
|
+
- [高危命令策略](#高危命令策略)
|
|
23
|
+
- [自定义规则(每台服务器独立)](#自定义规则每台服务器独立)
|
|
24
|
+
- [审批与两段式确认](#审批与两段式确认)
|
|
25
|
+
- [输出与后台任务](#输出与后台任务)
|
|
26
|
+
- [审计](#审计)
|
|
27
|
+
- [配置](#配置)
|
|
28
|
+
- [主机密钥与凭据](#主机密钥与凭据)
|
|
29
|
+
- [排错](#排错)
|
|
30
|
+
- [已知限制](#已知限制)
|
|
31
|
+
- [开发](#开发)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 它能做什么
|
|
36
|
+
|
|
37
|
+
| 你会用它做 | 怎么发生 |
|
|
38
|
+
|---|---|
|
|
39
|
+
| 让 agent 在服务器上查状态、看日志、跑脚本 | 模型调用 `srv_exec(server, command)`,插件经连接池执行,返回 exitCode、stdout、stderr |
|
|
40
|
+
| 避免 agent 手滑执行破坏性命令 | 20 条内置高危规则先判定:`block` 直接拒绝,`confirm` 弹审批框,`allow` 直接执行 |
|
|
41
|
+
| 事后追查谁执行了什么 | 每次 `srv_exec` 写一行 JSON 到 `audit.log`,面板里也能看到最近 20 条 |
|
|
42
|
+
| 长命令不阻塞对话 | 超过同步窗口自动转后台 job,模型用 `srv_job_output` 增量读取、`srv_job_kill` 取消 |
|
|
43
|
+
| 删服务器、关主机密钥校验这类危险改动先缓一缓 | 先进设置页的「待确认操作」,确认后才真正生效 |
|
|
44
|
+
|
|
45
|
+
**不做什么**:不做文件上传/SFTP、端口转发、跳板机、Kerberos(见[已知限制](#已知限制))。
|
|
46
|
+
另外,策略是**遏制不是安全边界**——真正的硬边界仍然是远端那个低权限用户与 sudo 白名单。
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 安装
|
|
51
|
+
|
|
52
|
+
四种方式,按推荐顺序排列。**装进哪个 profile 用 `--profile` 指定**,Web 界面即 `web`。
|
|
53
|
+
|
|
54
|
+
### 方式一:npm(推荐)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
dsh plugin --profile web add dsh-server-connector
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
指定版本:`dsh plugin --profile web add dsh-server-connector@0.1.0`。
|
|
61
|
+
|
|
62
|
+
国内网络若装不上或版本滞后,让 pnpm 走官方源:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
dsh plugin --profile web add --registry=https://registry.npmjs.org/ dsh-server-connector
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> npm 有约 24 小时的「新版本冷却期」(防供应链投毒),刚发布的版本可能暂时装不上,属正常。
|
|
69
|
+
|
|
70
|
+
### 方式二:GitHub Release 的 tarball(离线 / 锁定版本)
|
|
71
|
+
|
|
72
|
+
从 [Releases](https://github.com/niechao136/dsh-server-connector/releases) 下载
|
|
73
|
+
`dsh-server-connector-<version>.tgz`,然后:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
dsh plugin --profile web add ./dsh-server-connector-0.1.0.tgz
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
tarball 是**预构建**的,安装时不会执行任何构建脚本,因此 pnpm 不会要求
|
|
80
|
+
`allowBuilds` 授权——适合不想让第三方代码在安装时运行的场景。
|
|
81
|
+
|
|
82
|
+
校验下载完整性(可选):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 每个 Release 的说明里都附有 SHA-256
|
|
86
|
+
sha256sum dsh-server-connector-0.1.0.tgz # Linux / macOS
|
|
87
|
+
certutil -hashfile dsh-server-connector-0.1.0.tgz SHA256 # Windows
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 方式三:GitHub 源码
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
dsh plugin --profile web add github:niechao136/dsh-server-connector
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
锁到某个提交更稳(后续推送无法悄悄改变实际运行的内容):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
dsh plugin --profile web add github:niechao136/dsh-server-connector#<sha>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
本仓库的 `lib/` 是已提交的纯 JS,没有 `prepare` 构建脚本,所以 git 安装后立即可用,
|
|
103
|
+
不会被 pnpm ≥10 的构建脚本拦截。
|
|
104
|
+
|
|
105
|
+
### 方式四:本地 checkout(开发时)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
dsh plugin --profile web add /path/to/dsh-server-connector
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`dsh plugin add` 会把包加入 profile 依赖,并自动把它追加到 `package.json` 的
|
|
112
|
+
`dsh.profile.bundles`:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"dependencies": { "dsh-server-connector": "0.1.0" },
|
|
117
|
+
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-server-connector"] } }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
确认装好了:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
dsh --profile web --dump-config | grep -A2 dsh-server-connector
|
|
125
|
+
# - id: dsh-server-connector
|
|
126
|
+
# name: dsh-server-connector
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
卸载:`dsh plugin --profile web remove dsh-server-connector`(会同时取消 bundle 选择)。
|
|
130
|
+
|
|
131
|
+
### 方式五:Web 界面
|
|
132
|
+
|
|
133
|
+
1. 左侧栏进入 **插件**;
|
|
134
|
+
2. 右上角 **添加插件**;
|
|
135
|
+
3. 填入 `dsh-server-connector`(npm 名)、GitHub 地址或本地绝对路径,选择安装源,点 **安装**;
|
|
136
|
+
4. 安装完成后在「已安装插件」里确认开关是打开的。
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
装完若侧边栏没有出现「服务器」,重启一次 `dsh web` 再试。
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 版本与分发渠道
|
|
145
|
+
|
|
146
|
+
同一个版本号在四个渠道同时提供,内容一致:
|
|
147
|
+
|
|
148
|
+
| 渠道 | 地址 | 适用 |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| npm | `dsh-server-connector` | 日常安装,`dsh plugin add` 直接可用 |
|
|
151
|
+
| GitHub Release | 附 `.tgz` + SHA-256 | 离线环境、锁定版本、审计 |
|
|
152
|
+
| GitHub 源码 | `github:niechao136/dsh-server-connector` | 想跟源码或钉到某个提交 |
|
|
153
|
+
| 本地 checkout | 绝对路径 | 开发调试 |
|
|
154
|
+
|
|
155
|
+
国内镜像(npmmirror)会自动同步 npm 上的发布,通常几分钟内可用;若滞后,按上文
|
|
156
|
+
「方式一」加 `--registry=https://registry.npmjs.org/` 直连官方源。
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 五分钟上手
|
|
161
|
+
|
|
162
|
+
以 Web 界面 + 一台真实服务器为例:
|
|
163
|
+
|
|
164
|
+
1. **打开面板**:左侧栏点 **服务器**(插件注册的入口,见下图)。
|
|
165
|
+
2. **新增服务器**:点右上角 **新增**,填名称、主机、端口、用户名,选认证方式。
|
|
166
|
+
- 私钥:填**本机可读**的绝对路径(保存时会检查文件是否存在);私钥本身带 passphrase 时本插件不支持,请改用一个无口令的部署密钥;
|
|
167
|
+
- 密码:只写不回显,存进 DSH 凭据服务,`servers.json` 里只留引用;
|
|
168
|
+
- SSH agent:目前只写入认证类型,插件尚未把 agent socket 传给 `ssh2`,实际连接会报 `AUTH_FAILED`(见[已知限制](#已知限制));需要 agent 认证请先改用私钥。
|
|
169
|
+
3. **测试连接**:列表里点 **测试**,看到「已连接」即通(探测的四项目前显示 `unknown`,属已知缺陷,不影响连通性)。想更稳妥,直接让 agent 跑一条命令验证:
|
|
170
|
+
> 在「生产机-A」上执行 `df -h /`,看看根分区还剩多少
|
|
171
|
+
4. **之后每次都说清楚机器**:插件**不做会话绑定**,每次调用都显式传 `server`。
|
|
172
|
+
模型看到系统提示里的服务器清单,会自己带上。
|
|
173
|
+
|
|
174
|
+

|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 服务器面板
|
|
179
|
+
|
|
180
|
+
### 列表
|
|
181
|
+
|
|
182
|
+
每行显示名称、`用户名@主机:端口`、默认工作目录与连接状态:绿点/「已连接」表示连接池里有活连接,
|
|
183
|
+
灰点/「未连接」表示还没连或已被空闲回收。有待确认操作时,标题右侧会出现红色计数徽标。
|
|
184
|
+
|
|
185
|
+
行内操作:**启用/禁用开关**、**测试**(建立连接并跑一条探测命令)、**编辑**(打开表单)、
|
|
186
|
+
点名称进入详情。
|
|
187
|
+
|
|
188
|
+
### 启用与禁用
|
|
189
|
+
|
|
190
|
+
每行右侧的开关直接切换,**不弹确认**。禁用后:
|
|
191
|
+
|
|
192
|
+
- 这一行变灰并标注「已禁用」,但**仍在列表里**——随时可以把开关拨回来;
|
|
193
|
+
- `srv_list` **不再返回**它,系统提示里也**不再列出**它;
|
|
194
|
+
- 任何 `srv_*` 调用(`srv_exec`、`srv_connect`、`srv_policy_check`)都会返回
|
|
195
|
+
`SERVER_DISABLED`,提示你去面板启用。
|
|
196
|
+
|
|
197
|
+
**已连接的服务器不能禁用**:先点「断开」,开关才会可用。这是有意的——你要求的是
|
|
198
|
+
「以后别用这台」,不是「立刻掐掉正在跑的命令」。
|
|
199
|
+
|
|
200
|
+
启用/禁用都会写一条审计(`server-disabled` / `server-enabled`),不产生待确认记录。
|
|
201
|
+
|
|
202
|
+
### 详情
|
|
203
|
+
|
|
204
|
+
连接字段只读、逐条高危策略的当前取值、最近 20 条审计,以及底部的 **删除**(删除会先进待确认区)。
|
|
205
|
+
|
|
206
|
+

|
|
207
|
+
|
|
208
|
+
### 表单字段
|
|
209
|
+
|
|
210
|
+
| 字段 | 说明 |
|
|
211
|
+
|---|---|
|
|
212
|
+
| 名称 | 必填、唯一;可以中文 |
|
|
213
|
+
| 主机 / 端口 | 端口 1–65535 |
|
|
214
|
+
| 用户名 | 远端登录用户 |
|
|
215
|
+
| 认证方式 | 私钥 / 密码 / SSH agent |
|
|
216
|
+
| 私钥路径 | 仅私钥认证;必须是本机绝对路径且文件存在 |
|
|
217
|
+
| 密码 | 仅密码认证;只写不回显,编辑时留空表示保持原密码 |
|
|
218
|
+
| 主机密钥策略 | `accept-new`(默认)/ `verify` / `off`;改 `off` 会进待确认区 |
|
|
219
|
+
| 默认工作目录 | 执行 `srv_exec` 未传 `cwd` 时的目录,如 `/srv/app` |
|
|
220
|
+
| 高危策略 | 每条内置规则一行:`allow` / `confirm` / `block` / `停用`(内置 block 规则只提供 `confirm` / `block`) |
|
|
221
|
+
|
|
222
|
+
表单里内置 `block` 规则只能降为 `confirm`,**不能直接改成 `allow`**;选「停用」不会立即生效,
|
|
223
|
+
而是写入待确认区,确认后该规则才彻底失效。
|
|
224
|
+
|
|
225
|
+

|
|
226
|
+
|
|
227
|
+
表单里只保存**偏离默认值**的规则,所以 `servers.json` 里通常 `policy` 是空对象。
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Agent 工具
|
|
232
|
+
|
|
233
|
+
插件注册 6 个工具,都在系统提示里可见;当有服务器时,系统提示还会附上服务器清单:
|
|
234
|
+
|
|
235
|
+
```text
|
|
236
|
+
可运维服务器(调用 srv_exec 时必须传 server):
|
|
237
|
+
- srv-01 生产机-A devops@10.0.0.5:22 默认目录 /srv/app
|
|
238
|
+
- srv-02 测试机-B devops@10.0.0.6:22 默认目录 /srv/app
|
|
239
|
+
高危策略:service-restart=confirm(可用 srv_policy_check 预检)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| 工具 | 参数 | 作用 |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `srv_list` | — | 列出服务器:id、名称、主机、默认目录、是否已连接 |
|
|
245
|
+
| `srv_connect` | `server` | 建立或复用连接,并探测远端平台、内核、shell、sudo 可用性(探测字段目前为 `unknown`,见[已知限制](#已知限制)) |
|
|
246
|
+
| `srv_exec` | `server`, `command`, `cwd?`, `timeoutMs?`, `async?` | 执行命令 |
|
|
247
|
+
| `srv_job_output` | `jobId`, `offset?` | 增量读取后台输出,返回下一个偏移量 |
|
|
248
|
+
| `srv_job_kill` | `jobId` | 取消运行中的命令 |
|
|
249
|
+
| `srv_policy_check` | `server`, `command` | 只判定 `allow` / `confirm` / `block`,**不连接、不执行** |
|
|
250
|
+
|
|
251
|
+
### `server` 参数怎么写都行
|
|
252
|
+
|
|
253
|
+
按顺序尝试:精确 `id` → 精确名称(不区分大小写)→ 唯一前缀。
|
|
254
|
+
零命中报 `UNKNOWN_SERVER` 并列出全部服务器;多命中报 `AMBIGUOUS_SERVER` 并列出候选——
|
|
255
|
+
模型通常能靠错误信息自己纠正,不用你介入。
|
|
256
|
+
|
|
257
|
+
### 模型实际看到的返回
|
|
258
|
+
|
|
259
|
+
`df -h /` 的 `srv_exec` 结果:
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
srv-01 (生产机-A) exit 0 46 ms
|
|
263
|
+
Filesystem Size Used Avail Use% Mounted on
|
|
264
|
+
/dev/sda1 98G 61G 32G 66% /
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`srv_policy_check` 的预检结论(`rm -rf` 命中 block):
|
|
268
|
+
|
|
269
|
+
```text
|
|
270
|
+
BLOCK on srv-01 (生产机-A) — rule rm-recursive (递归删除)
|
|
271
|
+
command: rm -rf /srv/app/old
|
|
272
|
+
This command will not run: the rule blocks it. Ask the user to change the server policy if it is really needed.
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
同样的命令交给 `srv_exec` 会被直接拒绝,不会连接、不会执行:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
POLICY_BLOCKED: command blocked by rule rm-recursive (递归删除) on server 生产机-A (srv-01): rm -rf /srv/app/old Nothing was sent to the server. Ask the user to allow this rule for that server, or rephrase the command.
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
错误消息是单行:`错误码: 发生了什么 下一步怎么做`,模型据此分支处理。
|
|
282
|
+
|
|
283
|
+
异步命令立刻拿到 job id:
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
started background job ssh-3f9a1c2b7d4e5f60 on srv-01 (生产机-A)
|
|
287
|
+
command: tar czf /tmp/app.tgz /srv/app
|
|
288
|
+
Read it with srv_job_output, stop it with srv_job_kill.
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 什么时候用 `srv_policy_check`
|
|
292
|
+
|
|
293
|
+
命令不确定会不会被拦时先预检。它和 `srv_exec` 用的是同一个判定函数,所以结论不会互相打架:
|
|
294
|
+
|
|
295
|
+
> 先 `srv_policy_check` 看一下,没问题再执行
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 高危命令策略
|
|
300
|
+
|
|
301
|
+
### 内置 20 条规则
|
|
302
|
+
|
|
303
|
+
| id | 拦截对象 | 默认 |
|
|
304
|
+
|---|---|---|
|
|
305
|
+
| `rm-recursive` | `rm -r` / `rm -rf` / `rm --recursive`(`git rm -r` 除外) | block |
|
|
306
|
+
| `dd-write` | `dd of=/dev/…`、`mkfs` | block |
|
|
307
|
+
| `db-drop` | `DROP DATABASE` / `DROP TABLE` / `TRUNCATE`(含通过 psql、mysql 等执行) | block |
|
|
308
|
+
| `fork-bomb` | `:(){ :\|:& };:` | block |
|
|
309
|
+
| `shutdown` | `shutdown` / `poweroff` | block |
|
|
310
|
+
| `reboot` | `reboot` | block |
|
|
311
|
+
| `chmod-root` | 对 `/etc`、`/usr` 等系统路径递归 `chmod` | confirm |
|
|
312
|
+
| `chown-root` | 对系统路径递归 `chown` | confirm |
|
|
313
|
+
| `service-stop` | `systemctl stop` / `disable` | confirm |
|
|
314
|
+
| `service-restart` | `systemctl restart`、`docker restart` | confirm |
|
|
315
|
+
| `docker-prune` | `docker prune`、`docker rm -f` | confirm |
|
|
316
|
+
| `curl-pipe-sh` | `curl … \| sh` | confirm |
|
|
317
|
+
| `kill-all` | `kill -9 -1`、`pkill -9` | confirm |
|
|
318
|
+
| `passwd-edit` | 写 `/etc/passwd` / `/etc/shadow` | block |
|
|
319
|
+
| `crontab-edit` | `crontab -r`、写 `/etc/cron*` | confirm |
|
|
320
|
+
| `iptables-flush` | `iptables -F` | confirm |
|
|
321
|
+
| `git-force-push` | `git push --force`(`--force-with-lease` 除外) | confirm |
|
|
322
|
+
| `history-clear` | `history -c`、清 `~/.bash_history` | confirm |
|
|
323
|
+
| `truncate-log` | 截断 `/var/log/*` | confirm |
|
|
324
|
+
| `sudoers-edit` | 写 `/etc/sudoers*` | block |
|
|
325
|
+
|
|
326
|
+
### 判定语义
|
|
327
|
+
|
|
328
|
+
1. 命令行按 `&&`、`||`、`;`、`|`、换行拆成子命令,**每一段**都过一遍规则;
|
|
329
|
+
2. 命中多条取最严:`block > confirm > allow`;
|
|
330
|
+
3. 每台服务器可覆盖默认值,但内置 `block` 只能降为 `confirm`,**不能降为 `allow`**;
|
|
331
|
+
4. 要彻底放行某条规则,只能在表单里把该规则设为**停用**(写入 `disabledRules`),且这一步走待确认区;
|
|
332
|
+
5. 停用后的规则在 `srv_policy_check` 与 `srv_exec` 中同时消失——两者共用同一个判定函数;
|
|
333
|
+
6. 没有命中任何规则就是 `allow`。
|
|
334
|
+
|
|
335
|
+
> **提醒**:正则拦不住 `python -c "import shutil; shutil.rmtree('/')"` 这类变形。
|
|
336
|
+
> 请配合远端专用低权限用户与 sudo 白名单使用。
|
|
337
|
+
|
|
338
|
+
### 自定义规则(每台服务器独立)
|
|
339
|
+
|
|
340
|
+
内置 20 条不可能覆盖所有人的「危险」。服务器详情页的 **自定义规则** 区块可以给
|
|
341
|
+
**单台** 服务器补规则,不必改插件、不必等发版。
|
|
342
|
+
|
|
343
|
+
在详情页点 **新增规则**,填四项:
|
|
344
|
+
|
|
345
|
+
| 字段 | 说明 |
|
|
346
|
+
|---|---|
|
|
347
|
+
| 标签 | 审批理由里显示的名字 |
|
|
348
|
+
| 正则 | **JavaScript 正则**(见下方方言说明) |
|
|
349
|
+
| 默认档 | 只有 `confirm` / `block`;不允许 `allow`(放行是默认行为,写了没意义) |
|
|
350
|
+
| 样例命令 | 每行一条,**至少一条必须命中**,否则保存会被拒绝 |
|
|
351
|
+
|
|
352
|
+
先点 **试跑** 再点 **保存**。试跑会逐条告诉你「这条命令会不会被拦」,并且
|
|
353
|
+
**不写盘**——它和真正执行走的是同一个判定函数,所以结论不会不一致。
|
|
354
|
+
|
|
355
|
+
几个要点:
|
|
356
|
+
|
|
357
|
+
- **正则是 JavaScript 方言,不是 POSIX。** 用 `\s`、`\d`、`\b`;`[[:space:]]`
|
|
358
|
+
这类 POSIX 字符类在 JS 里**不生效**(会被当成普通字符)。写错了不会报错,
|
|
359
|
+
只是永远不命中——而「永不命中」会被样例检查挡下来,你在保存时就能看见。
|
|
360
|
+
- **规则 id 由服务端生成**(`custom-1`、`custom-2`…),面板不让你填,避免和内置 id 撞车。
|
|
361
|
+
- **新建/修改/删除不走待确认区**:它们只会让限制变严。唯一例外是「停用」,
|
|
362
|
+
等同于改 `disabledRules`,仍走两段式确认。
|
|
363
|
+
- **每台服务器最多 50 条**;正则最长 512 字符,且拒绝 `(a+)+` 这类嵌套量词(防正则回溯卡死)。
|
|
364
|
+
|
|
365
|
+
例如给一台跑 Docker 的机器加:
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
标签:删 Docker 卷
|
|
369
|
+
正则:docker\s+compose\s+down\s+-v
|
|
370
|
+
默认档:confirm
|
|
371
|
+
样例:docker compose down -v
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
之后 agent 执行 `docker compose down -v` 时会先弹审批,`srv_policy_check` 也会返回 `confirm`。
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 审批与两段式确认
|
|
379
|
+
|
|
380
|
+
### agent 执行时的三道闸门
|
|
381
|
+
|
|
382
|
+
```text
|
|
383
|
+
srv_exec(command)
|
|
384
|
+
├─ ① 策略判定:block → 直接返回 POLICY_BLOCKED(不连接)
|
|
385
|
+
│ confirm → ②
|
|
386
|
+
│ allow → ③
|
|
387
|
+
├─ ② 用户审批:弹确认框,只有「允许一次」才继续;拒绝/取消/无法询问 → POLICY_DENIED_BY_USER
|
|
388
|
+
└─ ③ 执行:连接池 → 执行 → 写审计 → 返回
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
审批走 DSH 原生 `ctx.approval`,确认框里带着命中规则、服务器名和完整命令原文。
|
|
392
|
+
会话审批策略为 `never` 时**不会弹框**,直接拒绝并说明原因——这是 DSH 的 fail-closed 行为。
|
|
393
|
+
|
|
394
|
+
组合里没有审批服务时(如某些无界面部署),`confirm` 一律按「无法确认」处理并拒绝,不会偷偷执行;
|
|
395
|
+
没有后台任务服务时,所有命令强制同步执行,`srv_exec` 也不再返回 `jobId`。
|
|
396
|
+
|
|
397
|
+
### 面板操作的「待确认区」
|
|
398
|
+
|
|
399
|
+
面板本身不在 turn 内,没法弹审批框,所以三类危险操作改成两段式:
|
|
400
|
+
先记入 `pending.json`(目标保持原样),你回到设置页看到完整上下文后再确认。
|
|
401
|
+
|
|
402
|
+
| 操作 | 请求后立即 | 确认后 |
|
|
403
|
+
|---|---|---|
|
|
404
|
+
| 删除服务器 | 服务器仍存在、仍可用 | 删除记录、断开连接、清凭据,审计写 `server-deleted` |
|
|
405
|
+
| 主机密钥改为 `off` | 仍按原策略校验 | 改为 `off`,审计写 `hostkey-disabled` |
|
|
406
|
+
| 停用某条高危规则 | 规则仍生效 | 加入 `disabledRules`,审计写 `rule-disabled` |
|
|
407
|
+
|
|
408
|
+
每条记录 24 小时后自动过期(`pendingTtlHours`),过期后必须重新申请;过期记录无法确认。
|
|
409
|
+
|
|
410
|
+

|
|
411
|
+
|
|
412
|
+
设置页同时只读展示当前生效的全局配置——这些值来自 profile 配置,改完需要重新加载插件。
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## 输出与后台任务
|
|
417
|
+
|
|
418
|
+
| 场景 | 行为 |
|
|
419
|
+
|---|---|
|
|
420
|
+
| 正常结束 | 返回 `exitCode` + stdout + stderr;非 0 时结果带 `code: EXIT_NONZERO` |
|
|
421
|
+
| 输出过大 | 每条流(stdout、stderr 各自)超过 `maxOutputChars` 即截断并标记 `TRUNCATED` |
|
|
422
|
+
| 超过同步窗口 | 同步等待 `syncTimeoutMs` 后仍在跑,就返回 `jobId`,后续用 `srv_job_output` 增量读 |
|
|
423
|
+
| 主动异步 | `srv_exec` 传 `async: true`,立刻返回 `jobId` |
|
|
424
|
+
| 超时 | `timeoutMs` 到点由**远端 watchdog 杀掉整个进程组**,返回 `code: TIMEOUT` |
|
|
425
|
+
| 取消 | `srv_job_kill` 请求取消;远端进程组同样被清理 |
|
|
426
|
+
| 读得比生产慢 | 环形缓冲丢字节时 `srv_job_output` 返回 `lossy: true` 与完整输出的落盘路径 |
|
|
427
|
+
| 连接掉线 | 命令未产生输出前掉线会自动重连并重放**一次**;已经有输出则不再重放(避免重复副作用),报 `CONNECT_FAILED` 并说明是执行中掉线 |
|
|
428
|
+
|
|
429
|
+
`cwd` 的处理:不传就用服务器的默认工作目录;插件在远端拼 `cd '<cwd>' && …`,
|
|
430
|
+
不经过本地 shell,所以引号与转义只在远端 shell 语义下生效。
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## 审计
|
|
435
|
+
|
|
436
|
+
`auditEnabled` 开启时(默认),每次 `srv_exec` 都写一行 JSON——`allow`、`confirm`、`block` 都写——追加到
|
|
437
|
+
`$DSH_HOME/server-connector/audit.log`:
|
|
438
|
+
|
|
439
|
+
```json
|
|
440
|
+
{"ts":"2026-11-05T02:14:51.311Z","sessionId":"demo-session","serverId":"srv-01","serverName":"本地演示机","command":"df -h /","decision":"allow","policyRule":null,"approved":null,"exitCode":0,"durationMs":85,"truncated":false}
|
|
441
|
+
{"ts":"2026-11-05T02:14:51.497Z","sessionId":"demo-session","serverId":"srv-01","serverName":"本地演示机","command":"rm -rf /srv/app/old","decision":"block","policyRule":"rm-recursive","approved":null,"exitCode":null}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- `confirm` 且审批通过时 `approved: true`,被拒绝时 `approved: false`;
|
|
445
|
+
- `block` 记录 `approved` / `exitCode` 为 `null`,表示命令没有离开本机;
|
|
446
|
+
- 非执行事件也写一行,用 `event` 字段区分:`server-deleted`、`hostkey-disabled`、`rule-disabled`;
|
|
447
|
+
- `auditMaxBytes > 0` 时超过阈值轮转一次,旧内容落到 `audit.log.1`(最多两份);
|
|
448
|
+
- 面板只读尾部若干条;要看全部直接 `tail -f` 或 grep。
|
|
449
|
+
|
|
450
|
+
审计写入失败只记日志,不会改变命令本身的执行结果。
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 配置
|
|
455
|
+
|
|
456
|
+
配置写在 profile 的 `cordis.patch.yml`(Web 界面里是「打开配置文件」),按插件行 id 覆盖:
|
|
457
|
+
|
|
458
|
+
```yaml
|
|
459
|
+
- id: dsh-server-connector
|
|
460
|
+
config:
|
|
461
|
+
commandTimeoutMs: 90000
|
|
462
|
+
auditEnabled: false
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
| 键 | 默认 | 说明 |
|
|
466
|
+
|---|---|---|
|
|
467
|
+
| `connectTimeoutMs` | `15000` | SSH 连接超时 |
|
|
468
|
+
| `commandTimeoutMs` | `60000` | 单条命令默认超时,工具参数 `timeoutMs` 可覆盖 |
|
|
469
|
+
| `syncTimeoutMs` | `20000` | 同步等待窗口,超过就转后台 job |
|
|
470
|
+
| `maxOutputChars` | `200000` | stdout、stderr **各自**的输出上限 |
|
|
471
|
+
| `idleTimeoutMs` | `600000` | 连接空闲多久后关闭 |
|
|
472
|
+
| `auditEnabled` | `true` | 是否写审计(关闭后命令照常执行) |
|
|
473
|
+
| `auditMaxBytes` | `0` | `0` 表示不轮转;大于 0 时超限轮转一次 |
|
|
474
|
+
| `pendingTtlHours` | `24` | 待确认记录的有效期 |
|
|
475
|
+
| `maxListedServers` | `8` | 系统提示里最多列几台服务器,其余折叠成「另有 N 台」 |
|
|
476
|
+
|
|
477
|
+
所有键都有默认值,空配置也能启动。改完保存后由 profile 重新加载;没有启用 HMR 的环境需要重启 `dsh web`。
|
|
478
|
+
|
|
479
|
+
### 数据文件
|
|
480
|
+
|
|
481
|
+
```text
|
|
482
|
+
$DSH_HOME/server-connector/
|
|
483
|
+
├── servers.json # 服务器记录(不含明文密钥)
|
|
484
|
+
├── known_hosts.json # 每台服务器的主机指纹
|
|
485
|
+
├── pending.json # 待确认的危险操作
|
|
486
|
+
└── audit.log # 执行审计(JSONL)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`servers.json` 里的 `id` 由插件分配(`srv-01`、`srv-02`…),改名不影响调用;
|
|
490
|
+
记录结构见设计文档 §3.1。文件是原子替换写入,`srv_list`、`srv_exec`、`srv_connect`、`srv_policy_check` 每次调用都会重新读一遍,
|
|
491
|
+
所以手改后**下一次调用就生效**,不必重启(手改不是推荐做法,面板会做校验)。
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 主机密钥与凭据
|
|
496
|
+
|
|
497
|
+
### 主机密钥
|
|
498
|
+
|
|
499
|
+
| `hostKeyMode` | 首次连接 | 指纹变了 |
|
|
500
|
+
|---|---|---|
|
|
501
|
+
| `accept-new`(默认) | 接受并记入 `known_hosts.json` | **拒绝连接**,报 `HOST_KEY_UNKNOWN` |
|
|
502
|
+
| `verify` | 直接拒绝(未知主机) | 拒绝 |
|
|
503
|
+
| `off` | 不校验 | 不校验(不安全,保存需走待确认区) |
|
|
504
|
+
|
|
505
|
+
指纹变更时错误信息会提示:确认是换过机器后,删除 `known_hosts.json` 里对应条目(或信任新指纹)再重试。
|
|
506
|
+
|
|
507
|
+
### 凭据
|
|
508
|
+
|
|
509
|
+
- 密码交给 DSH 凭据服务(`$DSH_HOME/.credentials.yaml`),`servers.json` 只存引用,如 `server-connector:srv-03:password`;
|
|
510
|
+
- 面板只显示「已设置 / 未设置」,不会回显明文;接口也不会返回密钥;
|
|
511
|
+
- 没有凭据服务时,密码认证会报 `NO_CREDENTIAL`,改用私钥认证即可;
|
|
512
|
+
- 私钥认证只记录**路径**,密钥内容由 `ssh2` 自己读,插件不缓存。
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## 排错
|
|
517
|
+
|
|
518
|
+
工具错误都以错误码开头,并附一句下一步提示:
|
|
519
|
+
|
|
520
|
+
| 错误码 | 含义 | 怎么办 |
|
|
521
|
+
|---|---|---|
|
|
522
|
+
| `CONNECT_FAILED` | 连不上:网络、端口、sshd | 检查主机/端口、远端 sshd 是否在跑、防火墙 |
|
|
523
|
+
| `AUTH_FAILED` | 认证失败 | 核对用户名、私钥路径/密码;用 agent 认证的记录目前必然失败,请改用私钥 |
|
|
524
|
+
| `HOST_KEY_UNKNOWN` | 主机指纹未知或已变更 | 确认是同一台机器后清理该条指纹或调整 `hostKeyMode` |
|
|
525
|
+
| `NO_CREDENTIAL` | 密码服务器没有存密码 | 在面板里补密码,或改用私钥/agent |
|
|
526
|
+
| `POLICY_BLOCKED` | 命中 block 规则 | 命令没有离开本机;确需执行请改服务器策略或换写法 |
|
|
527
|
+
| `POLICY_DENIED_BY_USER` | 审批被拒绝/取消/无法询问 | 换一种写法,或在会话里把审批策略调成 `ask` |
|
|
528
|
+
| `UNKNOWN_SERVER` | 服务器不存在 | 先 `srv_list`;错误里会列出全部可用服务器 |
|
|
529
|
+
| `AMBIGUOUS_SERVER` | 前缀匹配到多台 | 用完整 id 或完整名称 |
|
|
530
|
+
| `TIMEOUT` | 超时,远端进程组已被杀 | 调大 `timeoutMs`,或改成 `async: true` |
|
|
531
|
+
| `EXIT_NONZERO` | 命令执行了但退出码非 0(是结果里的 `code`,不是抛出的错误) | 看 stderr,命令本身的问题 |
|
|
532
|
+
| `CONNECT_FAILED`(执行中) | 命令跑到一半连接断开,已产生输出所以不重放 | 重试;频繁出现就检查网络与 sshd 稳定性 |
|
|
533
|
+
| `INVALID_ARGUMENT` | 参数不合法 | 修正参数 |
|
|
534
|
+
|
|
535
|
+
**面板「测试」一直失败**:先确认这台机器能 `ssh 用户名@主机`;私钥认证注意文件权限(`600`)。
|
|
536
|
+
|
|
537
|
+
**没有「服务器」入口**:确认 bundle 已选中(`--dump-config` 能看到该行),必要时重启 `dsh web`。
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 已知限制
|
|
542
|
+
|
|
543
|
+
- **高危策略不是安全边界**:正则只能拦住已知模式,`base64 -d | sh`、脚本内的破坏性调用都拦不住。请配合远端低权限用户与 sudo 白名单。
|
|
544
|
+
- **`srv_connect` 的探测字段目前恒为 `unknown`**:探测脚本在 POSIX shell 下有语法错误(`then;`),连接本身正常,`srv_exec` 不受影响;面板「测试」也因此只显示 `unknown`。修复另开改动。
|
|
545
|
+
- **SSH agent 认证尚未接通**:`auth.kind = "agent"` 的记录能保存,但 `lib/pool.js` 的 `#authFor` 没有把 `agent` socket 交给 `ssh2`,连接会以 `AUTH_FAILED` 结束(实测 `ssh2` 只有在显式收到 `agent: <SSH_AUTH_SOCK>` 时才用 agent)。需要 agent 认证时请暂时改用私钥。
|
|
546
|
+
- **私钥 passphrase 不支持**:记录里出现 `passphrase` 会被注册表直接拒绝(防止明文密钥落盘),请使用无口令部署密钥。
|
|
547
|
+
- **不支持跳板机 / ProxyJump**:目前是单跳直连。
|
|
548
|
+
- **不支持 Kerberos / GSSAPI**:`ssh2` 的限制;需要时可换后端。
|
|
549
|
+
- **一台服务器上的命令串行执行**:同一服务器的命令排队,不同服务器并行;连接掉线只自动重试一次。
|
|
550
|
+
- **系统提示里的服务器清单对所有会话可见**:这是去掉会话绑定后的取舍;超过 `maxListedServers` 台时折叠。
|
|
551
|
+
- **审计只能追加与读尾部**:没有查询与聚合能力,需要时自己 grep 或写脚本。
|
|
552
|
+
- **面板不在 turn 内**:所以面板上的危险操作只能走「待确认区」,不能复用审批弹框。
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## 开发
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
npm install # 或 pnpm install
|
|
560
|
+
npm test # node --test "tests/**/*.test.ts"
|
|
561
|
+
npm run typecheck # tsc --noEmit
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
测试分两层:单元测试覆盖策略、注册表、审计、待确认队列与工具面;集成测试跑真实
|
|
565
|
+
`ssh2` 服务端与真实审批服务,Docker 可用时还会起一个 OpenSSH 容器验证超时杀进程组。
|
|
566
|
+
|
|
567
|
+
- 设计规格:[`docs/design.md`](./docs/design.md)
|
|
568
|
+
- 界面截图与采集方式:[`docs/usage/README.md`](./docs/usage/README.md)
|
|
569
|
+
- 历史界面变更对照:[`docs/phase5/README.md`](./docs/phase5/README.md)
|
|
570
|
+
|
|
571
|
+
## License
|
|
572
|
+
|
|
573
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# dsh-server-connector bundle patch: inserts only this plugin into a profile's
|
|
2
|
+
# layer stack. `name` is the module specifier the loader imports, so it must
|
|
3
|
+
# equal package.json's `name` verbatim; `id` is the row handle in the config tree.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: dsh-server-connector
|
|
6
|
+
name: 'dsh-server-connector'
|
package/icon.svg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g transform="translate(1.5 1.5) scale(0.9166666667)">
|
|
3
|
+
<path d="M6 8.5C6 6.57 9.13 5 13 5H23C26.87 5 30 6.57 30 8.5V13.5C30 15.43 26.87 17 23 17H13C9.13 17 6 15.43 6 13.5V8.5Z" fill="url(#a)" fill-opacity="0.85" />
|
|
4
|
+
<path d="M6 22.5C6 20.57 9.13 19 13 19H23C26.87 19 30 20.57 30 22.5V27.5C30 29.43 26.87 31 23 31H13C9.13 31 6 29.43 6 27.5V22.5Z" fill="url(#a)" fill-opacity="0.45" />
|
|
5
|
+
<circle cx="12.5" cy="11" r="1.8" fill="#FFFFFF" />
|
|
6
|
+
<circle cx="12.5" cy="25" r="1.8" fill="#FFFFFF" />
|
|
7
|
+
<path d="M17 11H24M17 25H24" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" />
|
|
8
|
+
</g>
|
|
9
|
+
<defs>
|
|
10
|
+
<linearGradient id="a" x1="18" y1="5" x2="18" y2="31" gradientUnits="userSpaceOnUse">
|
|
11
|
+
<stop stop-color="#7AC2FF" />
|
|
12
|
+
<stop offset="1" stop-color="#4A65E8" />
|
|
13
|
+
</linearGradient>
|
|
14
|
+
</defs>
|
|
15
|
+
</svg>
|