dsh-remote-agent 0.0.0-stage → 0.4.1
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 +700 -0
- package/CONTRIBUTING.md +358 -0
- package/KNOWN-ISSUES.md +601 -0
- package/LICENSE +21 -0
- package/README.en.md +353 -0
- package/README.md +317 -2
- package/SECURITY.md +348 -0
- package/bin/dsh-remote-relay.js +257 -0
- package/bin/dsh-remote-setup.js +440 -0
- package/bin/setup.cmd +30 -0
- package/cordis.patch.yml +87 -0
- package/deploy/relay-vps/Caddyfile +24 -0
- package/deploy/relay-vps/README.md +132 -0
- package/deploy/relay-vps/dsh-remote-relay.service +47 -0
- package/deploy/relay-vps/nginx.conf +49 -0
- package/deploy/relay-vps/relay.env.example +20 -0
- package/docs/dispatch-automation-eval.md +140 -0
- package/docs/screenshots/panel-main.png +0 -0
- package/docs/screenshots/panel-task-detail.png +0 -0
- package/docs/verification-agent-e2e.md +425 -0
- package/docs/verification-repos-routing-real.md +521 -0
- package/docs/verification-resume-real.md +254 -0
- package/docs/worker-setup-pitfalls.md +266 -0
- package/lib/artifacts.js +451 -0
- package/lib/client.js +1752 -0
- package/lib/e2e.js +304 -0
- package/lib/glob.js +225 -0
- package/lib/hub/audit-log.js +161 -0
- package/lib/hub/hub-util.js +114 -0
- package/lib/hub/index.js +500 -0
- package/lib/hub/node-registry.js +943 -0
- package/lib/hub/pull-manager.js +781 -0
- package/lib/hub/registry-handshake.js +403 -0
- package/lib/hub/registry-ingest.js +346 -0
- package/lib/hub/relay-link.js +228 -0
- package/lib/hub/subagent-provider.js +424 -0
- package/lib/hub/templates.js +693 -0
- package/lib/hub/tools.js +1059 -0
- package/lib/hub/ui-routes.js +856 -0
- package/lib/hub/views.js +351 -0
- package/lib/identity.js +271 -0
- package/lib/index.js +272 -0
- package/lib/node/artifact-service.js +231 -0
- package/lib/node/diff.js +124 -0
- package/lib/node/exec.js +536 -0
- package/lib/node/hub-pin.js +71 -0
- package/lib/node/index.js +1022 -0
- package/lib/node/repos.js +134 -0
- package/lib/node/resource.js +63 -0
- package/lib/node/runner.js +792 -0
- package/lib/node/task-engine.js +712 -0
- package/lib/node/task-store.js +273 -0
- package/lib/node/transport.js +763 -0
- package/lib/pairing.js +186 -0
- package/lib/protocol.js +414 -0
- package/lib/relay-config.js +317 -0
- package/lib/relay.js +732 -0
- package/lib/setup-routes.js +313 -0
- package/lib/setup.js +522 -0
- package/lib/ws.js +466 -0
- package/package.json +103 -4
- package/profiles/README.md +573 -0
- package/profiles/hub.cordis.yml +84 -0
- package/profiles/worker.cordis.yml +60 -0
- package/scripts/check-doc-drift.mjs +802 -0
- package/scripts/check-packaging.mjs +177 -0
- package/scripts/diag-inject.mjs +243 -0
- package/scripts/e2e-live-check.mjs +181 -0
- package/scripts/relay-init.ps1 +286 -0
- package/scripts/relay-init.sh +199 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# dsh-remote-relay VPS 部署模板
|
|
2
|
+
|
|
3
|
+
一台公网小 VPS,把「双方都在 NAT 后」的 hub 与 node 会合到一起。
|
|
4
|
+
本目录是**可直接抄的部署模板**;想一条命令装完请用仓库里的
|
|
5
|
+
`scripts/relay-init.sh`(幂等,自动生成 unit 与凭证)—— 两者等价,
|
|
6
|
+
本目录的存在是为了让人**在装之前就能看到将要发生的一切**。
|
|
7
|
+
|
|
8
|
+
> 中继只做寻址与转发:不存任务、不碰产物字节、不解析业务帧。
|
|
9
|
+
> 它挂了任务照跑(副机本地持久执行,重连后 task/sync 对账)——
|
|
10
|
+
> 这是设计使然,见 profiles/README.md 的降级链一节。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 0. 装之前想清楚的三件事
|
|
15
|
+
|
|
16
|
+
1. **端口**:默认 `8890/tcp`。只给这两台机器用就把来源限到它们的出口 IP,
|
|
17
|
+
别对 `0.0.0.0/0` 开。
|
|
18
|
+
2. **TLS**:只要中继暴露在公网,就必须前置 TLS 用 `wss://`
|
|
19
|
+
(明文 `ws://` 下 token 与业务帧都在裸奔)。模板见
|
|
20
|
+
[`Caddyfile`](./Caddyfile)(自动证书,最省事)或
|
|
21
|
+
[`nginx.conf`](./nginx.conf)(已有证书/自有 nginx 时用)。
|
|
22
|
+
3. **凭证**:`hubToken` / `enrollToken` 一对,中继机器上生成,
|
|
23
|
+
**只走加密信道交给两端**。日志里只出现 12 位指纹,别把明文贴进工单。
|
|
24
|
+
|
|
25
|
+
## 1. 五分钟上手(推荐:一键脚本)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 在 VPS 上,进入包根(把仓库 scp/rsync 过去,或 git clone)
|
|
29
|
+
sudo ./scripts/relay-init.sh # 默认 8890 / 0.0.0.0;幂等
|
|
30
|
+
# RELAY_PORT=9000 sudo ./scripts/relay-init.sh # 换端口
|
|
31
|
+
# DRY_RUN=1 ./scripts/relay-init.sh # 只看动作不落盘
|
|
32
|
+
# sudo FORCE_ROTATE=1 ./scripts/relay-init.sh # 轮换凭证(已入网副机要重配)
|
|
33
|
+
|
|
34
|
+
# 拿明文 token(只打一次;别重定向进文件、别贴聊天记录)
|
|
35
|
+
node bin/dsh-remote-relay.js --print-tokens --data-dir /var/lib/dsh-remote-relay
|
|
36
|
+
# → RELAY_TOKENS hub=rt_... enroll=en_... source=file
|
|
37
|
+
|
|
38
|
+
# 自检
|
|
39
|
+
curl -fsS http://127.0.0.1:8890/healthz
|
|
40
|
+
# → {"ok":true,"protocol":1,"nodes":0,"hubs":0,"stats":{...}}
|
|
41
|
+
journalctl -u dsh-remote-relay -f # 看 RELAY_READY 与事件 JSON 行
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
脚本做三件事:建系统用户与数据目录(0700)→ 生成/复用凭证写
|
|
45
|
+
`/etc/dsh-remote-relay/env`(0600)→ 装 systemd unit 并 `enable --now`。
|
|
46
|
+
没有 systemd 的机器会打印可复制的 nohup 命令。重复执行不会轮换 token。
|
|
47
|
+
|
|
48
|
+
## 2. 手动安装(不用脚本时)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# ① 用户与目录
|
|
52
|
+
sudo useradd --system --home-dir /var/lib/dsh-remote-relay \
|
|
53
|
+
--create-home --shell /usr/sbin/nologin dsh-relay
|
|
54
|
+
sudo install -d -m 0700 -o root -g root /etc/dsh-remote-relay
|
|
55
|
+
sudo install -d -m 0700 -o dsh-relay -g dsh-relay /var/lib/dsh-remote-relay
|
|
56
|
+
|
|
57
|
+
# ② 凭证(env 模板抄自本目录 relay.env.example,改占位符)
|
|
58
|
+
sudo install -m 0600 -o root -g root relay.env.example /etc/dsh-remote-relay/env
|
|
59
|
+
sudo -u dsh-relay node bin/dsh-remote-relay.js \
|
|
60
|
+
--data-dir /var/lib/dsh-remote-relay # 首次自动生成 tokens.json
|
|
61
|
+
|
|
62
|
+
# ③ unit(模板抄自本目录 dsh-remote-relay.service,改 <占位符>)
|
|
63
|
+
sudo install -m 0644 dsh-remote-relay.service /etc/systemd/system/
|
|
64
|
+
sudo systemctl daemon-reload && sudo systemctl enable --now dsh-remote-relay
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 3. 防火墙 / 云安全组
|
|
68
|
+
|
|
69
|
+
| 云 | 做法 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| 阿里云 / 腾讯云 等安全组 | 放行 `tcp/8890`,来源填两台机器的出口 IP |
|
|
72
|
+
| `ufw` | `ufw allow from <副机IP> to any port 8890 proto tcp` |
|
|
73
|
+
| `firewalld` | 加 rich rule,source 限两台机器 |
|
|
74
|
+
| 裸机 iptables | `-p tcp --dport 8890 -s <IP> -j ACCEPT`,默认 DROP |
|
|
75
|
+
|
|
76
|
+
套了 TLS 前置时,反向代理监听 443,中继本体只听 `127.0.0.1`——
|
|
77
|
+
公网根本看不到 8890(`Caddyfile` / `nginx.conf` 模板即此形态)。
|
|
78
|
+
|
|
79
|
+
## 4. TLS 前置(公网必做)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Caddy(自动申请/续期证书;把 <域名> 换成你的)
|
|
83
|
+
sudo cp Caddyfile /etc/caddy/Caddyfile && sudo systemctl reload caddy
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# nginx(证书用自己的;两个模板都要抄:sites-available + include 段)
|
|
88
|
+
sudo cp nginx.conf /etc/nginx/sites-available/dsh-relay
|
|
89
|
+
sudo ln -s /etc/nginx/sites-available/dsh-relay /etc/nginx/sites-enabled/
|
|
90
|
+
sudo nginx -t && sudo systemctl reload nginx
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
套上之后两端地址全部换成 `wss://<域名>/relay`(端口 443 省略):
|
|
94
|
+
hub 的 `relay.url`、node 的 `relayUrl` 一起换,token 不变。
|
|
95
|
+
healthz 与 `/api/relay/*` 也走 443(模板已含 HTTP 透传)。
|
|
96
|
+
|
|
97
|
+
## 5. 验证连通
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# ① 中继活着(有 TLS 时走 https)
|
|
101
|
+
curl -fsS https://<域名>/healthz
|
|
102
|
+
|
|
103
|
+
# ② 副机入网后,节点目录(只读;token 用 hub 那一半)
|
|
104
|
+
curl -fsS "https://<域名>/api/relay/nodes?token=rt_xxx"
|
|
105
|
+
# → {"nodes":[{"nodeId":"slave-pc","os":"windows","since":...,"lastSeen":...}]}
|
|
106
|
+
|
|
107
|
+
# ③ hub 侧:DSH 里 remote_nodes → online:true
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**对指纹,不对明文**:中继日志的 `RELAY_READY ... hub=<12位> enroll=<12位>`
|
|
111
|
+
与两端配置的来源必须是同一套。`nodes:0` 但副机说 online → 两端 token 不是
|
|
112
|
+
一套;`hubs:0` → hub 的 `relay.url`/`relay.token` 不对。
|
|
113
|
+
|
|
114
|
+
## 6. 轮换、备份、卸载
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 轮换(会让已入网副机全部掉线,提前打招呼)
|
|
118
|
+
sudo FORCE_ROTATE=1 ./scripts/relay-init.sh
|
|
119
|
+
node bin/dsh-remote-relay.js --print-tokens --data-dir /var/lib/dsh-remote-relay
|
|
120
|
+
|
|
121
|
+
# 备份只需一个文件(凭证真身)
|
|
122
|
+
sudo tar czf relay-tokens.tgz /etc/dsh-remote-relay/env
|
|
123
|
+
|
|
124
|
+
# 卸载(数据目录里没有任务状态,删不心疼)
|
|
125
|
+
sudo systemctl disable --now dsh-remote-relay
|
|
126
|
+
sudo rm -f /etc/systemd/system/dsh-remote-relay.service
|
|
127
|
+
sudo rm -rf /etc/dsh-remote-relay /var/lib/dsh-remote-relay
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
*端到端配置片段与排障表见 [profiles/README.md](../../profiles/README.md)。*
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# dsh-remote-relay systemd unit 模板
|
|
2
|
+
#
|
|
3
|
+
# 用法:把 <尖括号> 占位符换成实际值,安装到
|
|
4
|
+
# /etc/systemd/system/dsh-remote-relay.service
|
|
5
|
+
# 或者直接用 scripts/relay-init.sh 一键生成等价比的一份(推荐)。
|
|
6
|
+
#
|
|
7
|
+
# 与一键脚本的差异只有一点:本文件把路径写成占位符,方便先审后装;
|
|
8
|
+
# relay-init.sh 生成的是填好值的同一份。
|
|
9
|
+
|
|
10
|
+
[Unit]
|
|
11
|
+
Description=dsh-remote-agent relay (addressing + forwarding only)
|
|
12
|
+
Documentation=https://github.com/zbzbhs/dsh-remote-agent
|
|
13
|
+
After=network-online.target
|
|
14
|
+
Wants=network-online.target
|
|
15
|
+
|
|
16
|
+
[Service]
|
|
17
|
+
Type=simple
|
|
18
|
+
# 运行身份:专用系统用户,nologin
|
|
19
|
+
User=dsh-relay
|
|
20
|
+
Group=dsh-relay
|
|
21
|
+
# 包根(bin/dsh-remote-relay.js 的上级目录)
|
|
22
|
+
WorkingDirectory=<PKG_DIR>
|
|
23
|
+
# 凭证与监听参数都来自 env 文件(0600,root:root);文件不存在会启动失败,
|
|
24
|
+
# 这是故意的——没有凭证的中继没有意义。
|
|
25
|
+
EnvironmentFile=/etc/dsh-remote-relay/env
|
|
26
|
+
ExecStart=<NODE_BIN> <PKG_DIR>/bin/dsh-remote-relay.js \
|
|
27
|
+
--port ${DSH_RELAY_PORT} --host ${DSH_RELAY_HOST} --data-dir ${DSH_RELAY_DATA_DIR}
|
|
28
|
+
|
|
29
|
+
Restart=always
|
|
30
|
+
RestartSec=3
|
|
31
|
+
KillSignal=SIGTERM
|
|
32
|
+
TimeoutStopSec=20
|
|
33
|
+
|
|
34
|
+
# ── 加固(与 relay-init.sh 生成的一致,勿随意删) ──
|
|
35
|
+
NoNewPrivileges=true
|
|
36
|
+
PrivateTmp=true
|
|
37
|
+
ProtectSystem=strict
|
|
38
|
+
ProtectHome=true
|
|
39
|
+
# 唯一可写路径:数据目录(tokens.json 在这里)
|
|
40
|
+
ReadWritePaths=/var/lib/dsh-remote-relay
|
|
41
|
+
|
|
42
|
+
StandardOutput=journal
|
|
43
|
+
StandardError=journal
|
|
44
|
+
SyslogIdentifier=dsh-remote-relay
|
|
45
|
+
|
|
46
|
+
[Install]
|
|
47
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# dsh-remote-relay 的 TLS 前置模板(nginx)
|
|
2
|
+
#
|
|
3
|
+
# 安装:sudo cp nginx.conf /etc/nginx/sites-available/dsh-relay
|
|
4
|
+
# sudo ln -s /etc/nginx/sites-available/dsh-relay /etc/nginx/sites-enabled/
|
|
5
|
+
# sudo nginx -t && sudo systemctl reload nginx
|
|
6
|
+
#
|
|
7
|
+
# 与 Caddyfile 模板二选一。nginx 不会自动签证书,<证书路径> 用你自己的
|
|
8
|
+
# (certbot / 云厂商证书均可)。
|
|
9
|
+
|
|
10
|
+
upstream dsh_relay {
|
|
11
|
+
server 127.0.0.1:8890;
|
|
12
|
+
keepalive 4;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
server {
|
|
16
|
+
listen 443 ssl;
|
|
17
|
+
http2 on;
|
|
18
|
+
server_name relay.example.com;
|
|
19
|
+
|
|
20
|
+
ssl_certificate /etc/ssl/certs/relay.example.com.fullchain.pem;
|
|
21
|
+
ssl_certificate_key /etc/ssl/private/relay.example.com.key;
|
|
22
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
23
|
+
|
|
24
|
+
# 中继是长连接 WS 转发,读超时必须放宽:副机空闲时也可能几分钟
|
|
25
|
+
# 没有业务帧(中继自己的 ping/pong 保活,见 lib/relay.js)。
|
|
26
|
+
proxy_read_timeout 1h;
|
|
27
|
+
proxy_send_timeout 1h;
|
|
28
|
+
|
|
29
|
+
location / {
|
|
30
|
+
proxy_pass http://dsh_relay;
|
|
31
|
+
|
|
32
|
+
# ── WebSocket 三件套(缺一不可) ──
|
|
33
|
+
proxy_http_version 1.1;
|
|
34
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
35
|
+
proxy_set_header Connection "upgrade";
|
|
36
|
+
|
|
37
|
+
proxy_set_header Host $host;
|
|
38
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
39
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
40
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# HTTP 一律跳 HTTPS(ACME challenge 若要走 80 需另行放行)
|
|
45
|
+
server {
|
|
46
|
+
listen 80;
|
|
47
|
+
server_name relay.example.com;
|
|
48
|
+
return 301 https://$host$request_uri;
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# dsh-remote-relay 环境文件模板
|
|
2
|
+
#
|
|
3
|
+
# 安装:sudo install -m 0600 -o root -g root relay.env.example /etc/dsh-remote-relay/env
|
|
4
|
+
# 然后编辑占位符。systemd unit 通过 EnvironmentFile= 读它。
|
|
5
|
+
#
|
|
6
|
+
# 权限必须是 0600 root:root —— 这里面是两个凭证的明文。
|
|
7
|
+
|
|
8
|
+
# 监听地址。套了 TLS 前置(Caddyfile / nginx.conf 模板)时填 127.0.0.1,
|
|
9
|
+
# 公网只看到反向代理的 443;不套 TLS 才填 0.0.0.0(并务必把来源限到两端 IP)。
|
|
10
|
+
DSH_RELAY_HOST=127.0.0.1
|
|
11
|
+
DSH_RELAY_PORT=8890
|
|
12
|
+
|
|
13
|
+
# 数据目录:tokens.json 落点(0700 dsh-relay:dsh-relay)
|
|
14
|
+
DSH_RELAY_DATA_DIR=/var/lib/dsh-remote-relay
|
|
15
|
+
|
|
16
|
+
# ── 凭证 ──────────────────────────────────────────────────────────────
|
|
17
|
+
# hub 端凭证:只有主电脑(hub)用,拨入时以 x-relay-token header 携带
|
|
18
|
+
DSH_RELAY_HUB_TOKEN=rt_REPLACE_WITH_GENERATED
|
|
19
|
+
# 入网凭证:副机(node)用;也支持一次性 token(管理接口铸造,15 分钟有效)
|
|
20
|
+
DSH_RELAY_ENROLL_TOKEN=en_REPLACE_WITH_GENERATED
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# 派发自动化评估:预授权令牌 vs 文档明示边界
|
|
2
|
+
|
|
3
|
+
**状态**:评估结论(2026-10-15),未实施任何代码改动。
|
|
4
|
+
**触发**:生态评估报告 P1-7 —— "当前派发受 DSH 会话围栏限制需人肉,
|
|
5
|
+
评估提供预授权令牌机制或文档明示该边界为设计使然"。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 结论(先行)
|
|
10
|
+
|
|
11
|
+
| 方案 | 决定 | 一句话理由 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **B. 文档明示边界** | **现在做** | 围栏是 pinned 的安全属性(doc-drift 11/12/13),当前无实证的自动化需求;先把边界写清楚,让用户知道脚本该走哪条路 |
|
|
14
|
+
| **A2. 短时令牌(桌面端铸造、脚本兑换)** | **预设计,不实施** | 设计正确、与仓内一次性 enroll token 同构;等出现真实需求再作为 minor 落地,本文第 4 节给出完整设计草图 |
|
|
15
|
+
| **A1. 静态 automationToken** | **明确拒绝** | 在公网可暴露的 hub 上放一个无过期、可移植的 bearer secret,泄露面与 `sshpass -p` 同级;没有场景需要"永久有效" |
|
|
16
|
+
|
|
17
|
+
**给用户的答案(一句话)**:派发任务的脚本化入口今天**故意不存在**——
|
|
18
|
+
hub 的 webserver 整台被 DSH 会话围栏保护(实测除 `/healthz` 外一律 401),
|
|
19
|
+
这是安全设计生效的证据。需要自动化时,驱动 DSH 自身的会话
|
|
20
|
+
(`remote_task` 工具)是唯一受支持的路径;纯 HTTP 脚本无路可走是**设计使然,
|
|
21
|
+
不是缺陷**(KNOWN-ISSUES §8 第 3 条同结论)。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. 问题定义
|
|
26
|
+
|
|
27
|
+
**派发自动化** = 不经人手、不看桌面面板,由脚本/CI/定时器向副机下达任务。
|
|
28
|
+
典型场景: nightly 构建后自动派测试任务、看板系统联动、批量数据整理。
|
|
29
|
+
|
|
30
|
+
**要评估的能力**:`POST /api/remote-agent/ui/dispatch` 对**非桌面会话**
|
|
31
|
+
调用方开放。注意这不是"读数据"——dispatch 是**下命令**:`mode: exec`
|
|
32
|
+
直接在副机跑 shell(SECURITY.md 明示"不限制副机能执行的命令"),
|
|
33
|
+
所以这道的后面是副机上的任意代码执行。
|
|
34
|
+
|
|
35
|
+
## 2. 现状事实链(全部可复现)
|
|
36
|
+
|
|
37
|
+
| # | 事实 | 证据 |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| 1 | UI 路由**刻意复用** DSH 框架围栏,不另造鉴权 | `lib/hub/ui-routes.js` makeGuard 注释:`requestRejection` 的官方用途就是 "apply ... to **another** Web route" |
|
|
40
|
+
| 2 | 围栏**失败关闭**:抛错 → 403,绝不放行 | 同文件 catch 分支;doc-drift 断言 12 锚定 |
|
|
41
|
+
| 3 | 无 connection 服务时退回 sameOriginOk:无 Origin 放行 / 跨站 403 | `lib/hub/ui-routes.js` sameOriginOk |
|
|
42
|
+
| 4 | **真实 DSH 运行时**:除 `/healthz` 外整台 401(桌面渲染器外) | KNOWN-ISSUES §8 第 3 条,DSH 0.2.0-rc.2 实测;`scripts/e2e-live-check.mjs` 文件头同记录 |
|
|
43
|
+
| 5 | 因此 e2e-live-check 的派发步骤只能人肉 | 同上线索 + KNOWN-ISSUES §8「这一步需要人的原因」 |
|
|
44
|
+
| 6 | 副机入网口(`/api/remote-agent/node`)对外开放,但 nodeToken 只让你**成为节点**(接收任务),不能派发 | `lib/hub/node-registry.js` attachDirect;profiles/README.md 路径二 |
|
|
45
|
+
| 7 | 今天唯一的脚本化派发路径 = 驱动 DSH 自身会话(remote_task 工具) | README 用法节;工具经 DSH agent loop,围栏内 |
|
|
46
|
+
|
|
47
|
+
**推论**:围栏挡住的不是"坏人"(那有 nodeToken/roots 管),而是
|
|
48
|
+
"非桌面会话的调用方"——包括脚本。而 hub 属主本来就能从桌面派任何任务,
|
|
49
|
+
所以围栏的真实语义是**「下令权绑定在桌面会话上」**,不是"下令权需要密码"。
|
|
50
|
+
|
|
51
|
+
## 3. 已存在的替代缓解(为什么现在不做也不堵)
|
|
52
|
+
|
|
53
|
+
1. **驱动 DSH 会话**:`remote_task` 是受支持的派发 API,自动化脚本可以
|
|
54
|
+
操作 DSH 的会话/工具层(DSH 自身能力,不归本插件管)。
|
|
55
|
+
2. **relay 的一次性 enroll token 已是同构先例**:短 TTL、用过即焚、
|
|
56
|
+
只存中继内存(`lib/relay.js` mintToken)——证明本仓有能力做好
|
|
57
|
+
"预授权"这回事,缺的不是技术,是需求信号。
|
|
58
|
+
3. **面板 pair-info 的 loopback 披露边界**:连"生成配对码"都只敢在
|
|
59
|
+
回环给全码(doc-drift 45)——插件对凭证披露的整体口径是收紧的,
|
|
60
|
+
单独给派发开一个Bearer大门与这个口径相冲。
|
|
61
|
+
|
|
62
|
+
## 4. A2 设计草图(预设计,供未来 minor 直接施工)
|
|
63
|
+
|
|
64
|
+
> 原则:**铸造必须经过桌面会话,兑换只给派发族路由,窗口短,可审计。**
|
|
65
|
+
|
|
66
|
+
### 4.1 形态
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
桌面面板/会话 ──POST /ui/automation-token(会话围栏内)──► hub 内存
|
|
70
|
+
返回 { token: 'dt_...', expiresInMs: 900000, maxUses: 1 } (默认 15 分钟 / 单次)
|
|
71
|
+
脚本 ──POST /ui/dispatch Header: x-dsh-remote-automation: dt_...──► 派发成功
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4.2 约束(缺一不可,全部进 doc-drift)
|
|
75
|
+
|
|
76
|
+
| 约束 | 原因 |
|
|
77
|
+
|---|---|
|
|
78
|
+
| 铸造路由在会话围栏内(不豁免) | 铸造权 = 桌面在座;豁免它就等于 A1 |
|
|
79
|
+
| 兑换只放行 `dispatch` / `cancel` 两条 | **绝不放行** `pair-info`(披露凭证)、`pull`(写盘)、`config`(配置视图)、四个只读路由 |
|
|
80
|
+
| token 只存内存,不落盘;TTL 默认 15min;默认单次 | 与 relay oneTime token 同策略(`lib/relay.js:118`),进程重启即失效 |
|
|
81
|
+
| 比较用 timingSafeEqual,失败关闭 | 复用 relay.js safeEqual 模式;围栏抛错 → 403 |
|
|
82
|
+
| 每次兑换写审计日志(nodeId + goal 长度 + token 指纹) | 可观测;不记 goal 正文(可能含秘密) |
|
|
83
|
+
| `hub` 公网暴露时 SECURITY.md 明示:等同把下令权交出去 | 与 direct 模式 TLS 要求同一口径 |
|
|
84
|
+
|
|
85
|
+
### 4.3 施工清单(≈1–1.5 人日,含测试与文档)
|
|
86
|
+
|
|
87
|
+
1. `lib/hub/ui-routes.js`:automation-token 铸造路由 + guard 的 token 分支
|
|
88
|
+
(scope 白名单硬编码,不读配置——**不给自己留"以后放宽"的开关**);
|
|
89
|
+
2. `lib/client.js`:面板加「生成自动化令牌」按钮(与副机接入卡片同风格);
|
|
90
|
+
3. 测试:铸造→兑换 happy path、过期拒、超用拒、错 token 401、
|
|
91
|
+
pair-info 持 token 仍 401、guard 抛错失败关闭;
|
|
92
|
+
4. doc-drift:scope 白名单断言(pair-info 永不在内)+ 铸造路由在围栏内;
|
|
93
|
+
5. SECURITY.md 新增「自动化令牌」节;KNOWN-ISSUES §8 第 3 条补链接。
|
|
94
|
+
|
|
95
|
+
### 4.4 工作量评估
|
|
96
|
+
|
|
97
|
+
| 部分 | 量 |
|
|
98
|
+
|---|---|
|
|
99
|
+
| 路由 + guard + 内存 TTL 表 | ~120 行 |
|
|
100
|
+
| 面板按钮 | ~40 行(复用 pair-info 模式) |
|
|
101
|
+
| 测试 + doc-drift | ~150 行 |
|
|
102
|
+
| 文档(SECURITY/profiles/CHANGELOG) | ~80 行 |
|
|
103
|
+
|
|
104
|
+
真正的成本不是代码是**评审负担**:这是本插件第一次在会话围栏上开洞,
|
|
105
|
+
每一行都要按"开洞"标准审。
|
|
106
|
+
|
|
107
|
+
## 5. A1 为什么明确拒绝
|
|
108
|
+
|
|
109
|
+
`automationToken` 静态配置看起来只多一行 config,但它是:
|
|
110
|
+
|
|
111
|
+
1. **公网面上的便携静态 secret** —— direct 模式的 hub 本就在公网,
|
|
112
|
+
bearer token 进 shell history / CI log / 代理访问日志的成本远比
|
|
113
|
+
桌面会话 cookie 低(cookie 不落地、随会话消亡);
|
|
114
|
+
2. **无过期、可无限次用** —— 泄露即永久交出下令权,而 dispatch 的
|
|
115
|
+
后面是副机 shell;
|
|
116
|
+
3. **与仓内口径相冲** —— relay 的一次性 token、pair-info 的 loopback
|
|
117
|
+
披露、icacls 0600,每个凭证决策都是"缩短暴露窗口"方向,A1 是唯一
|
|
118
|
+
反方向的;
|
|
119
|
+
4. **没有场景需要永久有效** —— 定时任务用 A2 每次铸造即可(或接受
|
|
120
|
+
"定时任务先调 DSH 会话")。
|
|
121
|
+
|
|
122
|
+
## 6. 文档明示边界(B)的落点
|
|
123
|
+
|
|
124
|
+
已写入 / 待写入:
|
|
125
|
+
|
|
126
|
+
- 本文档(决策记录 + A2 预设计);
|
|
127
|
+
- KNOWN-ISSUES §8 第 3 条:补链接到本文;
|
|
128
|
+
- README 安全节 / profiles/README 排障表:各加一行"脚本派发?
|
|
129
|
+
设计使然,见 docs/dispatch-automation-eval.md";
|
|
130
|
+
- 若未来实施 A2:CHANGELOG 记 minor,SECURITY.md 开节,本文档状态
|
|
131
|
+
改为"已实施"并指向实现。
|
|
132
|
+
|
|
133
|
+
## 7. 风险与复审触发条件
|
|
134
|
+
|
|
135
|
+
出现以下任一信号时复审本结论(直接按 §4 施工 A2):
|
|
136
|
+
|
|
137
|
+
- 有用户/Issues 明确要求脚本化派发(≥2 次独立请求);
|
|
138
|
+
- 出现"定时任务 + 副机"的真实场景描述(而非设想);
|
|
139
|
+
- DSH 自身提供了 headless 会话能力,使"驱动 DSH 会话"路径变轻
|
|
140
|
+
(此时 B 的缓解 #1 更强,可能继续不做)。
|
|
Binary file
|
|
Binary file
|