feat: add stdin script ssh routes

This commit is contained in:
Codex
2026-05-25 05:58:51 +00:00
parent 5d636d2d5b
commit 64c82b5be8
5 changed files with 132 additions and 33 deletions
+22 -10
View File
@@ -19,7 +19,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
- `server cleanup plan [--min-age-hours N] [--limit N]` 只生成主 server Docker 镜像清理 dry-run 计划,不执行删除;默认 `--min-age-hours 24`,避免把刚发布或刚验证的镜像列为 stale。输出必须包含 `dryRun=true``mutation=false``policy.deletionExecuted=false`、active containers/images、受保护镜像、candidate stale images、估算释放空间、风险等级、`commandsToReview` 和人工审批清单。计划必须保守白名单:保留 running containers 使用的 image ID,保留 stopped containers 引用的 image ID 直到人工先复核容器,保留 `deploy.json`/`CI.json` 当前 commit-pinned artifact、Compose stable image、上游 digest pin 和 provider-gateway runner image`protectedStorage` 必须显式列出 PostgreSQL named volume、Baidu Netdisk `.state`、D601 registry storage 和 Docker volumes/host data policy。该入口禁止生成或执行 `docker system prune``docker image prune``docker builder prune``docker volume rm``docker compose down -v`、数据库清理或 host data `rm` 命令;未来若增加真实删除,必须另设显式审批参数并先复核 dry-run 输出。
- `server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`;该命令用于替代手工删除容器的兜底流程,其中 `dev-frontend-proxy` 只更新主 server dev 入口薄代理,`todo-note``code-queue-mgr``project-manager``baidu-netdisk``oa-event-flow` 只重建主 server 承载的对应后端,不会重建或删除 database 命名卷。D601 Code Queue 执行面不由 `server rebuild` 管理,Rust backend-core 迭代不得用 `server rebuild backend-core` 在 master server 编译,规则见 `docs/reference/dev-environment.md`
- `provider attach <providerId> [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-<ID>.env` 默认只包含 `UNIDESK_MASTER_SERVER``PROVIDER_ID``provider-<ID>.yml` 固定 Docker socket、`pid: "host"``restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build``provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]` 是只读多信号健康裁决入口,会把单路径 `provider is not online`、SSH 超时、registry 失败和 service proxy 失败归类成 `runner-local-observation-gap``service-degraded``provider-degraded``global-blocker`。默认输出只返回裁决、scope、失败/降级/未知信号和有界 evidence 摘要,完整 evidence 必须显式加 `--full``--raw`;推荐交叉验证命令仍包含 `debug health``debug dispatch <providerId> host.ssh --wait-ms 15000``ssh <providerId> argv true``artifact-registry health --provider-id <providerId>``microservice health k3sctl-adapter``microservice health code-queue``codex tasks --view supervisor --limit 20`
- `ssh <route> [ssh-like args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601`,也可以扩展为 `provider:plane:entry-or-scope...`,例如 `D601:k3s:kubectl``D601:k3s:hwlab-dev:hwlab-cloud-api`。非交互远端命令优先使用 `ssh <providerId> argv ...`需要 shell 特性时用 `bun scripts/cli.ts ssh D601 argv bash -lc '<command>'`ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示 argv 重试和 provider triage 交叉验证。
- `ssh <route> [ssh-like args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601`,也可以扩展为 `provider:plane:entry-or-scope...`,例如 `D601:k3s:kubectl``D601:k3s:hwlab-dev:hwlab-cloud-api``D601:k3s:script:hwlab-dev:hwlab-cloud-api`。非交互远端命令优先使用 `ssh <providerId> argv ...`需要 shell 脚本、管道、变量或循环时优先使用 quoted heredoc 单步传输,例如 `bun scripts/cli.ts ssh D601 script <<'SCRIPT'``bun scripts/cli.ts ssh D601:k3s:script:<namespace>:<workload> <<'SCRIPT'`,把脚本走 stdin,而不是把脚本压成多层引号字符串。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示 stdin script/argv 重试和 provider triage 交叉验证。
- `ssh <providerId> apply-patch [tool args...] < patch.diff` 直接调用远端注入的 `apply_patch` 工具,并把本地 stdin 中的标准 `*** Begin Patch` / `*** End Patch` patch 流透传给目标节点。
- `ssh <providerId> py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。
- `ssh <providerId> skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills``.codex/skills`
@@ -113,13 +113,13 @@ GitHub issue/PR 写操作必须优先使用 `bun scripts/cli.ts gh issue|pr ...
`bun scripts/cli.ts ssh --help``bun scripts/cli.ts ssh <providerId> --help` 是本地 JSON 帮助命令,必须快速返回;不能把 `--help` 解析成 Provider ID,不能打开交互 shell,也不能等待 provider 会话。
`bun scripts/cli.ts ssh D518` 应表现为登录 D518 WSL 的 shell`bun scripts/cli.ts ssh D518 hostname` 应像 `ssh D518 hostname` 一样只输出远端命令结果并返回远端 exit code。Provider ID 前的目标选择由 UniDesk 节点清单决定,`-p``-i``-l``-o` 等传统 ssh 传输参数由 provider-gateway 部署配置统一管理,CLI 会兼容性消费这些参数但不会覆盖节点侧维护桥配置。指挥官、CI 预检和其他非交互流程不要依赖 ssh-like 自由拼接;标准写法是 `bun scripts/cli.ts ssh D601 argv true`或者在需要管道、变量展开和多条命令时使`bun scripts/cli.ts ssh D601 argv bash -lc '<command>'`
`bun scripts/cli.ts ssh D518` 应表现为登录 D518 WSL 的 shell`bun scripts/cli.ts ssh D518 hostname` 应像 `ssh D518 hostname` 一样只输出远端命令结果并返回远端 exit code。Provider ID 前的目标选择由 UniDesk 节点清单决定,`-p``-i``-l``-o` 等传统 ssh 传输参数由 provider-gateway 部署配置统一管理,CLI 会兼容性消费这些参数但不会覆盖节点侧维护桥配置。指挥官、CI 预检和其他非交互流程不要依赖 ssh-like 自由拼接;单进程标准写法是 `bun scripts/cli.ts ssh D601 argv true`多行 shell 逻辑标准写法是 quoted heredoc 单步调`bun scripts/cli.ts ssh D601 script <<'SCRIPT'`
core 只允许声明了 `host.ssh` capability 的 provider 使用 `ssh` 透传或 `host.ssh` dispatch;旧 provider 不支持该能力时必须快速失败并输出错误,不能把未知命令误判成 `echo` 成功。
本地 broker 默认等待 provider SSH 会话打开 60000ms,以便在目标节点同时有较多 microservice.http 任务时仍能建立维护会话;需要诊断慢连接时可用 `UNIDESK_SSH_OPEN_TIMEOUT_MS=<ms>` 临时调大,但最小有效值固定为 15000ms,避免把真实离线误判为长时间阻塞。
ssh-like 远端命令如果出现 `kex_exchange_identification``Connection closed by remote host`、provider session timeout 或 exit code 255CLI 会在原始 stderr 后追加一行 `UNIDESK_SSH_HINT { ... }`。该 JSON 不回显原始远端命令,只包含 `code=ssh-like-command-friction``trigger``try``triage``try` 固定指向 `bun scripts/cli.ts ssh D601 argv bash -lc '<command>'` 形态,避免把一次 ssh-like 解析/握手摩擦误读成 D601 SSH 整体不可用。
ssh-like 远端命令如果出现 `kex_exchange_identification``Connection closed by remote host`、provider session timeout 或 exit code 255CLI 会在原始 stderr 后追加一行 `UNIDESK_SSH_HINT { ... }`。该 JSON 不回显原始远端命令,只包含 `code=ssh-like-command-friction``trigger``try``triage``try` 固定指向 stdin script 形态,避免把一次 ssh-like 解析/握手摩擦误读成 D601 SSH 整体不可用。
`ssh <providerId>` 会在远端会话启动时注入 `/tmp/unidesk-ssh-tools/apply_patch``/tmp/unidesk-ssh-tools/glob``/tmp/unidesk-ssh-tools/skill-discover`,并把该目录加入远端 `PATH``apply_patch` 接受标准 `*** Begin Patch` / `*** End Patch` patch 格式,便于通过 SSH 透传编辑远端仓库文件;`glob` 在远端用 Python 执行路径匹配,避免依赖 shell glob 展开;`skill-discover` 用于列出远端 Linux/WSL 与 Windows skill。目标节点需要具备 `python3``base64`。注入工具只写 `/tmp/unidesk-ssh-tools`,不修改目标仓库,交互式 shell 和远端命令都可以直接调用这些工具。
@@ -144,6 +144,18 @@ printf 'import sys\nprint(sys.argv)\n' | bun scripts/cli.ts ssh D601 py foo '--b
`ssh <providerId> py` 的附加参数是脚本参数,不是 Python 解释器参数;如需 `-m``-X` 或多条 shell 命令,仍使用原始远端命令入口。为了保证 CLI 输出及时可见,helper 固定采用“临时文件 + `python3 -u`”模式;provider 命令模式不分配 TTY,因此脚本内容不应被远端回显。
如果远端逻辑需要 shell 特性,不要再把整段脚本作为命令字符串传入。正式入口是 `bun scripts/cli.ts ssh D601 script`,脚本正文从 stdin 进入;CLI 会把本地 stdin 直接送到远端 `sh -s --``--shell bash` 可切换为 bash`--` 后的内容会作为脚本参数传入。临时单步执行优先用 quoted heredoc,复用脚本时才用 `< script.sh` 文件重定向。典型用法:
```bash
cat <<'SCRIPT' | bun scripts/cli.ts ssh D601 script --shell bash -- alpha
set -euo pipefail
printf 'arg=%s\n' "$1"
hostname
SCRIPT
```
这个入口的目标是分布式调试的“0 shell-command-string”路径:本地 shell 只负责 heredoc/stdinUniDesk 只负责 provider 路由,远端 shell 只解释脚本正文。脚本正文里仍然要遵守 shell 语言自身的规则,但不再穿过本地 shell、远端 shell、kubectl exec 和容器 shell 的多重字符串转义。
`ssh <providerId> skills` 是远端 skill 发现入口,也可写作 `ssh <providerId> skill discover`。输出固定为 JSON,包含 `node``roots``counts``skills``roots` 会显示每个候选 skill 根目录是否存在、扫描到多少 skill 以及错误;`skills` 会给出 `scope``name``description``path``skillMd` 和可转换时的 `windowsPath`。默认扫描远端用户的 `~/.agents/skills``~/.codex/skills`、可访问的 `/root/.agents/skills``/root/.codex/skills`;如果目标是 WSL,还会扫描 `/mnt/c/Users/*/.agents/skills``/mnt/c/Users/*/.codex/skills`,从而一次性看清 WSL 和 Windows 两套 skill。常用参数是 `--scope wsl``--scope windows``--limit N``--max-depth N``--root <path>``--windows-root <path>`;不要用宽泛的 Linux `find /mnt/*` 扫 Windows 盘,优先用这个结构化入口避免卡在 Windows 挂载层。
```bash
@@ -165,29 +177,29 @@ bun scripts/cli.ts ssh D601 find /home/ubuntu --max-depth 4 --type d --icontains
bun scripts/cli.ts ssh D601 glob --root /home/ubuntu/pikapython --pattern '**/*-test.cpp' --limit 20 --sort
```
`ssh` 的 route 语法是 `provider:plane:entry-or-scope...`。当前稳定 plane 是 `k3s``D601:k3s` 表示 D601 原生 k3s 控制面;`D601:k3s:kubectl` 表示该控制面的 kubectl CLI`D601:k3s:<namespace>:<workload>[:container]` 表示 namespace 下的一个默认 deployment workload,后续 argv 默认通过 `kubectl exec` 进入该对象;如果目标是具体 Podworkload 段写成 `pod/<podid>`,如果目标是 Deployment,也可以显式写 `deployment/<name>` 或简写 `<name>`
`ssh` 的 route 语法是 `provider:plane:entry-or-scope...`。当前稳定 plane 是 `k3s``D601:k3s` 表示 D601 原生 k3s 控制面;`D601:k3s:kubectl` 表示该控制面的 kubectl CLI`D601:k3s:<namespace>:<workload>[:container]` 表示 namespace 下的一个默认 deployment workload,后续 argv 默认通过 `kubectl exec` 进入该对象;`D601:k3s:script:<namespace>:<workload>[:container]` 表示把本地 stdin 脚本送入目标 workload。若目标是具体 Podworkload 段写成 `pod/<podid>`;若目标是 Deployment,也可以显式写 `deployment/<name>` 或简写 `<name>`
该 route 入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制容器命令组装成 kubectl argv,并固定远端 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml``D601:k3s` 无后续参数时执行 native k3s guard`D601:k3s:kubectl` 接收原始 kubectl argv`D601:k3s:logs:<namespace>:<workload>` 读取有界日志;`D601:k3s:exec:<namespace>:<workload>``D601:k3s:<namespace>:<workload>` 进入目标 workload。典型用法:
该 route 入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制容器命令和 stdin script 路由组装成 kubectl argv,并固定远端 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml``D601:k3s` 无后续参数时执行 native k3s guard`D601:k3s:kubectl` 接收原始 kubectl argv`D601:k3s:logs:<namespace>:<workload>` 读取有界日志;`D601:k3s:exec:<namespace>:<workload>``D601:k3s:<namespace>:<workload>` 进入目标 workload`D601:k3s:script:<namespace>:<workload>` 把本地 stdin 作为 pod 内 shell 脚本执行。典型用法:
```bash
bun scripts/cli.ts ssh D601:k3s
bun scripts/cli.ts ssh D601:k3s:kubectl get pods -n hwlab-dev
bun scripts/cli.ts ssh D601:k3s:logs:hwlab-dev:hwlab-cloud-api --tail 80
bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'
bun scripts/cli.ts ssh D601:k3s:hwlab-dev:pod/hwlab-cloud-api-abc:api sh -lc 'printf "%s\n" "$HOSTNAME"'
printf 'printf "pod=%s\n" "$HOSTNAME"\n' | bun scripts/cli.ts ssh D601:k3s:script:hwlab-dev:hwlab-cloud-api
```
route logs 默认是有界读取;`--follow`/`-f` 会被拒绝,防止 CLI 长时间占用维护桥。route exec 不要求手写 `--`CLI 会把 route 后续 argv 放到 `kubectl exec --` 后;如果命令本身需要复杂 shell 语法,把它作为容器内 shell 的单个 argv,例如 `sh -lc 'printf "%s\n" "$HOSTNAME"'`。这比把整条 `kubectl exec ... -- sh -lc ...` 放进远端 `bash -lc` 更稳定,因为本地 CLI 只需要处理一层容器内 shell 字符串。
route logs 默认是有界读取;`--follow`/`-f` 会被拒绝,防止 CLI 长时间占用维护桥。route exec 不要求手写 `--`CLI 会把 route 后续 argv 放到 `kubectl exec --` 后;如果命令本身需要复杂 shell 语法,优先改用 route script,把脚本走 stdin,而不是把 `kubectl exec ... -- sh -c ...` 放进远端命令字符串。
`ssh <providerId> argv <command> [args...]` 是通用 argv 安全拼接入口;`exec` 是同义入口。它是非交互远端命令的默认成功路径,不需要 shell 管道时直接传命令和参数,例如 `bun scripts/cli.ts ssh D601 argv true`需要管道、重定向、变量展开或多条命令时使用 `bun scripts/cli.ts ssh D601 argv bash -lc '<command>'`,让 shell 脚本作为 `bash -lc` 的一个 argv token 传递`find``glob``apply-patch` 有专用入口;`rg``grep``sed``nl``stat``du``ls``cat``head``tail``wc``pwd` 可以直接作为 `ssh` 子命令使用,CLI 会对每个 argv token 做 shell quoting。旧的自由 ssh-like 远端命令入口只保留为近似原生 ssh 的人工兼容路径。
`ssh <providerId> argv <command> [args...]` 是通用 argv 安全拼接入口;`exec` 是同义入口。它是非交互远端单进程命令的默认成功路径,不需要 shell 管道时直接传命令和参数,例如 `bun scripts/cli.ts ssh D601 argv true`需要管道、重定向、变量展开或多条命令时,优先改用 `ssh <providerId> script <<'SCRIPT'``find``glob``apply-patch` 有专用入口;`rg``grep``sed``nl``stat``du``ls``cat``head``tail``wc``pwd` 可以直接作为 `ssh` 子命令使用,CLI 会对每个 argv token 做 shell quoting。旧的自由 ssh-like 远端命令入口只保留为近似原生 ssh 的人工兼容路径。
通过 `ssh <providerId>` 执行多行脚本时,优先使用结构化 helper,例如 `bun scripts/cli.ts ssh D601 py < script.py``printf ... | (bun scripts/cli.ts ssh D601 'bash -s')` 这种单层 stdin 传输。不要在远端命令字符串里再嵌套 heredoc、复杂引号或 `ssh 'python3 - <<EOF ...'` 形态;多层 shell 解析容易把 stdin 绑定到错误进程,结果会打开远端交互解释器并留下悬挂的 broker/SSH 会话。长脚本需要复用时,优先通过 stdin 写入目标节点的临时脚本,再在同一个远端命令中显式执行并清理
通过 `ssh <providerId>` 执行多行脚本时,优先使用结构化 helper,例如 `bun scripts/cli.ts ssh D601 py < script.py``bun scripts/cli.ts ssh D601 script <<'SCRIPT'``bun scripts/cli.ts ssh D601:k3s:script:hwlab-dev:hwlab-cloud-api <<'SCRIPT'`。不要在远端命令字符串里再嵌套 heredoc、复杂引号或 `ssh 'python3 - <<EOF ...'` 形态;多层 shell 解析容易把 stdin 绑定到错误进程,结果会打开远端交互解释器并留下悬挂的 broker/SSH 会话。长脚本需要复用时,优先提交到 repo 或通过 stdin 传输到目标节点执行
## Remote Main Server Passthrough
`--main-server-ip` 是一个全局前缀,必须放在需要透传的命令同一次调用中,例如 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`。默认传输是公网 frontend:本地 CLI 读取本仓库 `config.json` 中的 frontend 登录账号密码,登录 `http://<ip>:<frontendPort>/` 获取 HttpOnly session cookie,然后通过 frontend 的 `/api/*` 同源代理访问 backend-core 内网 API;因此计算节点只需要能访问公网 frontend,不需要主 server SSH key,也不需要打开 backend-core REST API 或 PostgreSQL 端口。
默认 frontend 传输支持 `debug health``debug dispatch``debug task``artifact-registry status|health``ci publish-user-service --dry-run``microservice list/status/health/diagnostics/tunnel-self-test/proxy``decision upload/list/show/health``decision requirement list/upsert``decision diary import/list/history/months/show/edit/upsert``codex task <taskId>``codex tasks``codex unread``codex queues``codex output <taskId>``codex judge <taskId> --attempt N``ssh <PROVIDER_ID> <remote-command>``microservice status/health/diagnostics` 经 frontend 远程传输时也复用本地 CLI 的默认 compact summary`microservice health code-queue` 只有显式 `--raw``--full` 才返回完整健康 body。运行中纠偏 `codex steer` 属于 active run write control,应在主 server 本机 CLI 或显式 SSH 传输上执行,避免公网 frontend 透传限制 stdin/body 审计语义。其中 `ssh` 的 remote frontend 传输使用 `host.ssh` dispatch 执行有界远端命令,非交互命令同样优先 `ssh D601 argv true``ssh D601 argv bash -lc '<command>'`;交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。frontend 远程透传不会流式转发本地 stdin,因此 `ssh py < script.py``ssh apply-patch < patch.diff` 这类 stdin-backed helper 必须在主 server 本机运行,或显式切换到 `--main-server-transport ssh`。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--main-server-key <key>``--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts <command>`
默认 frontend 传输支持 `debug health``debug dispatch``debug task``artifact-registry status|health``ci publish-user-service --dry-run``microservice list/status/health/diagnostics/tunnel-self-test/proxy``decision upload/list/show/health``decision requirement list/upsert``decision diary import/list/history/months/show/edit/upsert``codex task <taskId>``codex tasks``codex unread``codex queues``codex output <taskId>``codex judge <taskId> --attempt N``ssh <PROVIDER_ID> <remote-command>``microservice status/health/diagnostics` 经 frontend 远程传输时也复用本地 CLI 的默认 compact summary`microservice health code-queue` 只有显式 `--raw``--full` 才返回完整健康 body。运行中纠偏 `codex steer` 属于 active run write control,应在主 server 本机 CLI 或显式 SSH 传输上执行,避免公网 frontend 透传限制 stdin/body 审计语义。其中 `ssh` 的 remote frontend 传输使用 `host.ssh` dispatch 执行有界远端命令,非交互单进程命令优先 `ssh D601 argv true`;需要 stdin script`py``apply-patch` 这类 stdin-backed helper 必须在主 server 本机运行,或显式切换到 `--main-server-transport ssh`交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--main-server-key <key>``--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts <command>`
计算节点可以用该入口测试自身的远程升级闭环,而不需要在计算节点公开 core REST API 或 database。标准顺序是:先运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health` 确认主 server 看到当前 Provider 在线,且该 Provider labels 中 `unideskCapabilities` 包含 `host.ssh``hostSshConfigured=true``hostSshKeyPresent=true`;再运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> provider.upgrade --mode schedule --wait-ms 15000` 触发真实 `provider.upgrade`;随后再次运行 `debug health` 确认节点重新上线;最后运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000``bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh <PROVIDER_ID> hostname` 验证 SSH 透传能力。provider-gateway 新部署或升级后没有完成这组 remote CLI 自测,不能视为交付完成。