交接单按归属约定(正文落文档库、工作区只放指针)归档至 归档/交接单-20260924-归档/,含逐件判定 README: - 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26) - 4 件主题已被覆盖网络线入口汇总 - 7 件历史接续包/规划件 CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、 工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
23 KiB
交接单 · 覆盖网络落地执行(v2 · 2026-09-16 15:2x 复核后重写)
给执行会话:按本单开工,不读规划会话的上下文。 v2 改了什么:新增 §0 复核结论 + 6 条勘误(其中 A/B 为实测,改变了 S3 的做法);更正部署面(v1 只写 106,漏了 47);步骤改为 P1 对齐 47 → P2=S2 → P3=S3(改写) → P4=S4。 方案来源 = 工作区根
会合中继拆分_取证与改造方案_20260916.md—— 必读 §4 + §8 + §9。 用户的验收要求(原话口径):改造后 47 + 106 + 本机都能跑起来;现有功能与多用户机制正常。 分层规则见代码仓docs/architecture.md(新模块必须守;npm run check:layering会在verify里拦你)。
0. 本轮复核结论(2026-09-16 15:2x · 全部只读实测)
0.1 已核实基线(部署前先照这个对一遍)
| 项 | 实测值 |
|---|---|
| 本机 | HEAD 640813e,工作区干净;lib/net/{reachability,rendezvous}.js 已 build(09-16 14:34) |
| 47(Manager) | dshs / dshs-worker / dshs-pg 全 active;Manager 跑 /opt/dshs/lib/cli.js(EnvironmentFile=/etc/dshs.env + drop-in dshs.service.d/cluster.conf) |
| 47 代码版本 | 🔴 落后于 S0 —— /opt/dshs/lib/ 无 net/;config.js 指纹 91fc3756 ≠ 本机 1c3601f7(lib mtime 09-16 00:39) |
| 106(Worker) | dshs-worker active;/healthz ⇒ {"ok":true,"hostId":"w-106","instances":0,"tunnel":{"ready":true,"ports":[19000]}} |
| 106 代码版本 | ✅ S1 三件已上:config.js 1c3601f7、worker/tunnel.js 5f2649b4 == 本机;lib/net/ 无(无需,那是 Manager 侧文件) |
dsh_hosts |
w-106 → http://127.0.0.1:19000(cap 2048)|w-47 → http://127.0.0.1:19100(cap 1024);共 7 列,无 via |
| 用户 / 实例 | admin@w-47、dbg2mx897@w-106、guest@w-106、pocuimwkrr@w-106;⚠️ 四条实例全 stopped ⇒ 验收要主动 launch |
| 47 sshd | gatewayports no|allowtcpforwarding yes|permitopen any|OpenSSH 8.0p1(106 = 9.3p2);lo = 127.0.0.1/8 |
| 47 端口面 | 0.0.0.0:32022 sshd(会合)|127.0.0.1:19000 sshd(106 隧道落点)|127.0.0.1:19100 node(w-47 agent)|127.0.0.1:15432 postgres |
0.2 勘误(6 条 · A/B 为实测,🔴 照原方案字面做会白做或出事)
A 🔴 DSHS_RELAY_LOCAL_NAMESPACE(回环别名)实测无效 ⇒ S3 换做法。
实测:自 106 发起 ssh -R 127.0.0.2:19999:127.0.0.1:19000 -o ExitOnForwardFailure=yes ⇒ 47 上落点实际是 127.0.0.1:19999(另含 [::1]:19999),127.0.0.2 被 sshd 静默改写,ssh 侧零报错(ExitOnForwardFailure 未触发、日志为空;测试后已清理)。
根因:47 sshd -T ⇒ gatewayports no(默认)⇒ sshd 强制把远端转发绑到回环,客户端指定的绑定地址被丢弃。
⇒ S3 改为「实例端口区间隔离」(§3 P3):同样打掉根因,且不改 sshd、不改协议、不需要 portMap。
将来若确需回环别名,只能开 GatewayPorts clientspecified —— 且只能开在新建的 relay sshd 上,⛔ 绝不动主 sshd 32022(那等于让持隧道密钥者可绑 0.0.0.0,命中 R5)。
B 🔴 端口撞号是真 bug**,不是"管道美化" —— 这才是 S3 的真实必要性。**
findFreePort()(src/supervisor/spawn.ts:53,唯一调用点 src/supervisor/orchestrator.ts:595)在 worker 自己那台机器上随机取端口 ⇒ 两台 worker 取到同号是常态;而 tunnel.forward() 的返回值在两处都被忽略(src/worker/agent.ts:303、:205)⇒ 撞号时 -R 失败但静默;Manager 仍按 127.0.0.1:<port> 拨 ⇒ 打到另一个用户的实例。w-47 的本地实例与 w-106 的隧道落点共享 47 的 127.0.0.1 端口空间 ⇒ 只用两台机器就能触发。
C 🔴 部署面:v1 只写了 106,漏了 47。
S0 的 src/web/server.ts、src/supervisor/remote-spawner.ts、src/net/* 跑在 Manager(47);S2 的 hostsProvider 也在那儿。
⇒ v1 说的"S1 已上线"只覆盖 Worker 侧;47 至今是 S0 之前的版本。执行时必须双侧对齐(§3 P1)。
D ⚠️ verify-cluster-cross.mjs 不能照抄跑,且有生产副作用**。**
① 默认 MANAGER=http://127.0.0.1:13080 = 演练端口(现役是 3080);AGENT_TOKEN 默认 cross-machine-token(现役 dshs-worker-7f3a91c05e);还需 ADMIN_PW(真实管理员口令)。
② 它会 幂等注册 w-106 并把容量改成 4096、新建 crossuser<ts> 用户并审批、在 106 上拉真实例,可选注册 w-106b 并迁移实例 ⇒ 跑完必须清理(§5.2)。
③ 脚本头部注释"控制面 PG 也在 106"已过时 —— PG 现在在 47(127.0.0.1:15432)。
E ⚠️ S4 有方案缺口:中继一旦不在 Manager 主机上,"Manager 拨落点"就断。
隧道落点在接收方那台机器的 127.0.0.1。中继换到别的机器 ⇒ Manager 拨不到 ⇒ 需要额外一跳(Manager 侧向中继建出向隧道 ssh -L,或中继侧把落点暴露到非回环地址 + nft 收窄)。
⇒ 本轮 S4 只做同机 relay(47,独立单元 / 端口 / 密钥 / 账号);异地 relay 留待定这一跳。
⛔ 不要用方案 §4 S4 的"会合指向第二个中继实例"当通过判据 —— 那要先解决本缺口;本轮改判据(见 §3 P4)。
F ⚠️ S2 减负:不要加 address 列。
endpoint 本身就是"要拨的地址",再加 address = 同义双真相(回填后必然逐字相等)。⇒ 只加 via(NOT NULL DEFAULT 'manager-ssh'),回填用一条显式 UPDATE。
方案 §8 的 ①②③ 仍然有效,其中 ① 本轮再次确认:
proxy.ts:109是发给上游 dsh 的Host头(dsh 的/api信任栅栏要 loopback),⛔ 绝不能改;真正的连接目标在proxy.ts:136-137(host/port取自endpointFor)。
1. 只读前置(必做,全部只读)
- 抢锁:
bash dsh-server-docs/scripts/handoff-guard.sh --claim-exec "<会话名>" - 读方案 §4 + §8 + §9;读
docs/architecture.md §5(现存 5 条违规基线,别新增) - 照 §0.1 表逐项复核基线(本机 HEAD|47 的
/etc/dshs.env+dshs.service.d/cluster.conf|106 的/etc/dshs-worker.env|dsh_hosts现值|47 端口面) - 远程入口:
ssh [email protected](22)|ssh test106(必须-i ~/.ssh/id_ed25519_test106)
2. 范围
做:P1 → P2 → P3(P4 视 P1–P3 结果决定是否本轮)
不做:⛔ 不换传输协议(WireGuard / TURN 属远期)|⛔ 不动 proxy.ts:109|⛔ 不动权威状态(归属 / 租约 / 骨干资格仍只在 Manager 写)|⛔ 不做 DSHS_RELAY_LOCAL_NAMESPACE(勘误 A)|⛔ 不加 portMap(不再改"不同号")
3. 步骤(每步独立可验、独立可回滚)
P1 · 对齐 47 到 HEAD 640813e(把 S0+S1 真正补上 Manager 侧)
为什么先做这一步:47 现在落后一代,直接上 S2 就是"跳版部署",出问题无从二分。这一步本身零行为变化,风险最低。
- 本机门禁(Node 22,24 会因 better-sqlite3 ABI 全红):
npm run build→npm test(须 44 项 / 0 失败)→npm run verify→npm run check:layering(现存违规 5 条,不得新增) - 备份 47(回滚点必须存在):
ssh [email protected] 'tar czf /opt/dsh/backups/lib-pre-S0-$(date +%Y%m%d-%H%M).tgz -C /opt/dshs lib' - 只传这几件(=
git show --name-only 640813e的src/清单 →lib/产物;同名.d.ts一起传;lib/net/需先mkdir -p):⛔ 别整包覆盖lib/config.js lib/net/reachability.js lib/net/rendezvous.js lib/supervisor/remote-spawner.js lib/web/server.js lib/worker/agent.js lib/worker/tunnel.jslib/—— 会把别人直接改在服务器上的内容回退。 - 逐文件 md5 对账(47 上算完与本机比,必须逐条相等;
.d.ts也核) systemctl restart dshs(会短暂中断门户;本项目为开发环境服务器 ⇒ 直接做,动手前一句话说明在动什么)
- 验收:① 47 三单元 active ② 门户
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3080/= 200 ③ 106/healthztunnel.ready=true④admin(w-47)与guest(w-106)各自的实例页都能打开 + 工作区文件能读能写 ⑤ls /opt/dshs/lib/net/有两个文件且 md5 == 本机 - 回滚:
systemctl stop dshs→ 用步骤 2 的 tgz 还原lib→systemctl start dshs
P2 · S2:dsh_hosts 增 via 列(打掉 C3)
- 顺序(不能颠倒):先加列(带默认值)→ 再改代码 → 最后回填 —— 任一步中断都不崩
- 加列:
src/db/schema.ts的MIGRATIONS追加 v8,双方言都要写:- sqlite:
ALTER TABLE dsh_hosts ADD COLUMN via TEXT NOT NULL DEFAULT 'manager-ssh'; - pg:同款 SQL(语法一致)
- sqlite:
- 改代码(6 处):
文件 改什么 src/db/pg.ts:56、src/db/repo.ts:598HOST_COLS末尾加, viasrc/db/pg.ts:605-646、src/db/repo.ts:603-625upsert 带上 viasrc/db/types.tsDshHost/UpsertDshHostInput加via?: string(缺省语义 =manager-ssh)src/web/routes/admin.ts:181-203注册口接受 via,缺省manager-sshsrc/web/server.ts:256-266hostsProvider:读via→RendezvousRegistry.get(via)→resolve(id)→ 塞ClusterHost.reachability;via未知/未设 ⇒ 回退parseReachability(id, endpoint)src/net/rendezvous.tsLocalRendezvous/ManagerSshRendezvous的addressOf由调用方从db.listDshHosts()建 map 传入(resolve是 async,hostsProvider本来就是 async ⇒ 无需改签名) - 回填(一条显式 SQL,幂等):
UPDATE dsh_hosts SET via='local' WHERE id='w-47';—— 判据:Manager 与 worker 同机 ⇒ 直连 ⇒local;w-106保持默认manager-ssh(47 上 sshd 的隧道落点)。⛔ 别一刀切(方案 §8 勘误 ②)。 - 部署:只改 47(S2 全是 Manager 侧代码,106 本轮无需改):传
lib/{config,net/*,db/*,web/*}对账 →systemctl restart dshs - 验收:
① 单测扩展
test/reachability.test.mjs⇒ 断言「迁移前agentUrl」== 「迁移后resolve()结果」逐条相等,两条真实行写死(w-47→local、w-106→manager-ssh) ② 真机/api/admin/hosts返回带via,且不含agentToken③ P1 的四条业务验收重跑一遍(多用户不受影响) - 回滚:列有默认值 ⇒ 旧代码读
endpoint完全不受影响,列可留着不删
P3 · S3(改写):实例端口区间隔离
- 目标:消除勘误 B 那条真 bug —— 跨 worker 同号 ⇒ 47 的
127.0.0.1端口空间撞号 ⇒ 静默不转发 ⇒ 拨到别人的实例 - 改哪些文件:
文件 改什么 src/supervisor/spawn.ts:53findFreePort()→ 增可选取址(区间内挑端口;区间为空 ⇒ 保持旧listen(0)行为)src/supervisor/orchestrator.ts:595取端口改走区间(读 config) src/config.ts新增 DSHS_INSTANCE_PORT_BASE(默认0= 旧行为)/DSHS_INSTANCE_PORT_SPAN(默认1000) - ⚠️
src/worker/agent.ts:352不用改(instanceHost仍127.0.0.1)—— 这就是本方案相对原设计的简化点;也因此不需要portMap - env 分配(必须互不重叠):
w-47→ base42000|w-106→ base43000|span 各1000 - 部署:47 ——
lib/{config,supervisor/*,worker/agent.js}+ drop-in 加两行 env →systemctl restart dshs dshs-worker;106 ——lib/{config,supervisor/*}+/etc/dshs-worker.env加两行 env →systemctl restart dshs-worker - 验收(三条端到端,缺一不可):
① 跨机实例页能打开 ② 跨机工作区文件能读能写 ③ 同一时刻 47 与 106 各起一个实例 ⇒
ss -lntp上 47 的127.0.0.1:w-47实例端口 ∈42000+、w-106实例端口 ∈43000+,且无重叠 - 回滚:删两个 env ⇒ 回
listen(0)随机(零代码回滚) - 不做:
DSHS_RELAY_LOCAL_NAMESPACE(勘误 A)|portMap(不再改"不同号")
P4 · S4(本轮收尾,可延后):中继独立成单元
- 做什么:47 上新建
dshs-relay.service= 独立 sshd 实例(独立端口如32023/ 独立 host key / 独立authorized_keys/ 独立系统账号dshsrelay;gatewayports no保持不动) - worker 侧只改一行 env:
DSHS_RENDEZVOUS_URL=ssh://[email protected]:32023 via补一个实现:新增RelayRendezvous(id 如relay:47-32023),hostsProvider经注册表选到它;w-106的via由manager-ssh改指它- 验收(三条,已按勘误 E 改判据):
① 单中继下全链路通(实例面 + 文件面)
② 停掉中继 ⇒ 同机形态(
w-47)实例仍可用、跨机代理失败但不崩 ③ ⛔ 不判"第二个中继实例";改判:把会合地址换回32022后,106 能重新注册并被正确解析(本轮能验的"会合可换"就到这里) - 回滚:
DSHS_RENDEZVOUS_URL指回32022(或删掉它,走DSHS_TUNNEL_TARGET兜底)+systemctl disable --now dshs-relay
4. 本机(客户端类型节点,不是 worker)
要验的是可行性评估缺口 1(全案唯一无证据的技术点):
- 本机起实例(
soft模式 —— Windows 无 bwrap/systemd)⇒ 实例页 200 - 实例内跑一次 bash 工具 ⇒ 验 dsh 在 Windows 的沙箱后端行为
- 本机 ↔ 47 / 106 的真实网络画像(NAT 类型 / 打洞可行性 / RTT + jitter)⇒ 直接填"最该先测三项"里的两项
⚠️ ⛔ 不得把本机接进现有生产链路(要独立形态、独立开关);本机是唯一开发机 ⇒ 用最小配额。
5. 全局验收(缺一 = 没做完)
5.1 通过项
- 多用户机制:
admin(锚w-47)、guest(锚w-106)各自实例页 + 工作区文件都正常(用户硬要求)—— ⚠️ 当前四条实例全stopped,要主动 launch verify-cluster-cross.mjs全绿 —— 必须按勘误 D 的参数跑:MANAGER=http://127.0.0.1:3080 AGENT_TOKEN=dshs-worker-7f3a91c05e ADMIN_PW=<真实管理员口令> node scripts/verify-cluster-cross.mjs(AGENT2留空 ⇒ 跳过第二台 worker 段)npm test+npm run verify全绿,且npm run check:layering无新增违规- 47:
dshs/dshs-worker/dshs-pgactive;106:dshs-workeractive - 每步的回滚点写清并实测过(至少回滚命令能跑)
5.2 跑完必须清理(勘误 D 的副作用)
- 删掉
crossuser*用户;把dsh_hosts里w-106的capacity_mb改回 2048 - 若跑过
AGENT2段:删掉w-106b登记,并把被迁移的实例改回w-106 - 自建的测试实例:
stop掉,别留在生产
6. 回报格式
判定 → 改了什么(文件 + 部署到哪台哪个路径 + md5 对账结果)→ 验收实测输出 → 未做 / 风险 → 锁状态 (截图或命令原文均可;⛔ 不许写"应该没问题")
7. 开工前已知的坑(省你踩一遍)
| 坑 | 事实 |
|---|---|
| 106 连不上 | 必须 -i ~/.ssh/id_ed25519_test106;其它 key 一律 Permission denied |
| 47 的 cluster 配置 | 在 drop-in dshs.service.d/cluster.conf,不在 /etc/dshs.env |
47 的 /opt/dshs 是 git 仓库 |
⚠️ 但不是部署来源 —— 部署 = 本机 build 后 scp lib/ 文件;47 上 git log 只有初始提交 + 一堆 M ⇒ ⛔ 别在 47 上 git pull。git -C /opt/dshs 会报 dubious ownership,要加 -c safe.directory=/opt/dshs |
| 47 的 PG 查询 | 只能 TCP:PGPASSWORD=dshs_cluster_2026 psql -h 127.0.0.1 -p 15432 -U dshs -d dshs;sudo -u postgres psql 不通(unix socket 未启用) |
C4 其实不成立 |
106/47 的 worker env 都没有 DSHS_TUNNEL_STATIC_PORTS ⇒ 隧道不含控制面 PG ⇒ 回滚面比方案预估小 |
| 本机 git | refs/remotes/** 写不进去 ⇒ git fetch 静默失败 ⇒ 分叉判定必须 git ls-remote 取裸 sha |
| 本机 bash | PATH 被 shim 重置 ⇒ 先 export PATH=<PortableGit>/usr/bin:<PortableGit>/bin:/c/Windows/System32:/c/Windows:$PATH;⛔ 进程替换 diff <(a) <(b) 不可用 |
| 本机跑测试 | 必须 Node 22(Node 24 会因 better-sqlite3 ABI 全红) |
8. 执行记录(2026-09-16 15:5x–16:2x · P1/P2/P3 已完成并验收)
执行会话:
覆盖网络-落地执行。P4 未做 —— 理由见 §8.4(命中 R5,需授权)。
8.1 ✅ P1 · 47 对齐到 640813e(Manager 侧补上 S0+S1)
- 前置取证:全量产物指纹对账 ⇒ 47 与本地 HEAD 的差异恰好只有 S0+S1 那 7 个文件,无其他漂移。
⚠️ 踩坑记录:本机
md5sum输出hash *path(二进制模式)、远端是hash path(文本模式)⇒ 直接diff会得到"59 个文件全不同"的假象。判据必须先归一化路径字段。 - 备份:
/opt/dsh/backups/lib-pre-S0-20260916-1555.tgz - 部署:7 个
.js+ 同名.d.ts/.js.map(共 21 件)⇒ 47/opt/dshs/lib/,属主/权限197108:197121 644(与现存一致) - 验收实测:7 件 md5 逐条相等|
dshs/dshs-worker/dshs-pgactive|门户 200|w-47与w-106agent 均可达(106 经隧道,tunnel.ready=true)|日志 0 错误 - 业务验收(多用户,R4 临时会话):
·
admin(w-47)实例running+ 实例页 200 ·guest(w-106)launch 成功 + 跨机实例页 200 +/api/desktop/tree通 +mkdir落盘已核实写在 106 的盘上 - 痕迹清理:临时会话
DELETE 2⇒余 0|/tmp临时件已删|guest 实例还原为stopped
8.2 ✅ P2 · dsh_hosts 增 via(打掉 C3)
- 改 7 处:
db/schema.ts(v8 迁移,双方言同写)|db/types.ts(DshHost.via+UpsertDshHostInput.via?+toDshHost)|db/pg.ts/db/repo.ts(HOST_COLS+ upsert)|web/routes/admin.ts(注册口接受via)|web/server.ts(hostsProvider:读via→RendezvousRegistry→resolve()→ClusterHost.reachability)|test/reachability.test.mjs(+2 条判据) - 两处按勘误 F 落地:不加
address列(endpoint本身就是要拨的地址,加列=同义双真相);via省略时不覆盖已有值(COALESCE(?, dsh_hosts.via))—— 否则一次不带via的 join 会把回填好的local冲回默认 - 回填(显式、幂等):
UPDATE dsh_hosts SET via='local' WHERE id='w-47';⇒ 结果w-47=local/w-106=manager-ssh✅ - 门禁:
buildrc=0|单测 46 项 / 0 失败|check:layering无新增违规|迁移后schema_migrations= 1–8,dsh_hosts列含via(默认manager-ssh) - 验收实测:
GET /api/admin/hosts⇒"via":"manager-ssh"/"via":"local",且不含agentToken(只有hasToken)|两条路径实例页均 200(w-47 同机 / w-106 跨机)|日志 0 错误 - 补做的一处:v1 交接单只要求"API 返回带 via",实测发现 GET 列表原本不下发
via⇒ 已补(否则运维看不见 = 加列白加)
8.3 ✅ P3(改写版)· 实例端口区间隔离
- 按勘误 A 执行:⛔ 未做
DSHS_RELAY_LOCAL_NAMESPACE(实测被 sshd 静默改写)|⛔ 未加portMap - 区间选定(避开 OS 临时端口段
32768-60999):w-47= 20000, span 1000|w-106= 21000, span 1000 - 改 4 处:
config.ts(instancePortBase/Span+toPortNumber防 NaN)|supervisor/spawn.ts(findFreePortInRange+findInstancePort)|supervisor/orchestrator.ts(取端口改走区间)|新增test/instance-port.test.mjs(5 条,已挂进npm test/verify) - 部署:两台(47
/opt/dshs、106/opt/dshs-cluster)各 3 件,md5 逐条相等;env 加在各自/etc/dshs-worker.env;备份lib-pre-S3-*.tgz+dshs-worker.env.bak-* - 门禁:
buildrc=0|单测 51 项 / 0 失败|check:layering无新增违规 - 验收实测(缺一不可的三条全过):
① 两台各起一个实例 ⇒
w-47落 20000、w-106落 21000 ② 47 上127.0.0.1:20000(node 直连) 与127.0.0.1:21000(sshd 隧道落点) 互不重叠 ⇒ 原先的撞号根因消除 ③ 实例页admin(同机) 200 +guest(跨机) 200,跨机文件面通 - 状态还原:
guest回stopped(与开工前一致);admin实例保持运行(与开工前一致)|临时会话已删|40 端口重启后旧端口(42185/37523/35765)全部释放
8.4 ⏳ P4 未做 —— 命中 R5,需要一句授权
做什么:47 上新建独立 dshs-relay.service(独立 sshd、独立端口 32023、独立 host key、独立 authorized_keys、非 root 账号 dshsrelay),106 的 DSHS_RENDEZVOUS_URL 指过去。
为什么停在这里:它要在公网面新开一个 SSH 监听口(32023) —— 属 **R5「权限只准收窄」**的反面(新暴露面),必须先知会。其余三项(P1–P3)都是"收窄/修复"性质,故直接做了。
P4 的收益与代价(供判断):
- 优点:与宝塔面板共用的主 sshd
32022解耦(面板改配置不再波及覆盖网络)|隧道密钥从root的authorized_keys挪到独立文件与非 root 账号|是"中继可多实例/可换机"的前置 - 缺点:新增一个公网 SSH 入口(尽管只允许密钥 +
restrict,port-forwarding)|受勘误 E 限制,异地中继仍差"一跳"未设计 ⇒ 本轮只能验同机形态
回滚:DSHS_RENDEZVOUS_URL 指回 ssh://[email protected]:32022 + systemctl disable --now dshs-relay。
8.5 本轮顺带确认的两件事(未改,仅记录)
- 控制面已在 PG 上:
DSHS_DEPLOY_MODE=cluster+DSHS_DB_URL=postgres://…:15432/dshs;dshs 进程未打开任何dshs.db(该文件 mtime 停在 09-15 17:51)= 纯回滚副本。⇒ 不存在"需要改造为 PG"这件事。 - 隧道密钥已受约束:47 的
authorized_keys里dshs-tunnel-106to47那行带restrict,port-forwarding⇒ 该密钥只能做端口转发、拿不到 shell(另两把是部署键与个人键,无约束属预期)。 ⚠️dsh_instances.status不反映实时状态:实测 admin 的实例在跑,DB 里却是stopped。运行态以 worker agent 的/status/:userId为准(Worker 按设计不写控制面数据)—— 排查时别拿这张表的status当事实。