# HWLAB DEV Runtime Hotfix Runbook

本文定义 `hwlab-dev` 运行态热修的最低边界、审计字段、只读确认和回滚口径。它只适用于 DEV 能力边界或阻塞排障，不是发布验收手册，也不把运行态覆盖当作源码真相。

本规则承接 [pikasTech/HWLAB#462](https://github.com/pikasTech/HWLAB/issues/462) 和 [pikasTech/HWLAB#465](https://github.com/pikasTech/HWLAB/issues/465)，服务 [pikasTech/HWLAB#7](https://github.com/pikasTech/HWLAB/issues/7) / [pikasTech/HWLAB#239](https://github.com/pikasTech/HWLAB/issues/239) 的“用户工作台 + Code Agent 真实可用”主线。它不改变 `DC-DCSN-P0-2026-003` / [pikasTech/HWLAB#78](https://github.com/pikasTech/HWLAB/issues/78) 的 M3 上位约束：hotfix runbook、审计输出、SOURCE、LOCAL、DRY-RUN 或只读观察都不能被包装成 M3 DEV-LIVE 通过。

## 允许条件

DEV runtime hotfix 只能在以下条件同时满足时使用：

- 目标是 `hwlab-dev`，不是 PROD、真实用户生产面或真实硬件生产控制面。
- 目的是打通能力边界、定位阻塞层或恢复用户工作台关键路径；不能为了绕过源码、测试、artifact 或 CD 流程而热修。
- 操作前已经明确该任务不会抢占 M3 虚拟硬件可信闭环主线，也不会把热修证据升级成 M3 验收。
- 操作后必须创建或关联源码化 follow-up。#465 的持久化拆分是固定分工：#460 负责 Code Agent -> PC gateway `shell.exec` 能力路由，#461 负责 hotfix runner 源码化，#462 负责本 runbook 与只读审计，#463 负责 JSON-RPC `serviceId` 结构化诊断，#464 负责 DEV-LIVE smoke/harness。

## G14 k3s 前置

当前 HWLAB DEV 运行态热修目标是 G14 `hwlab-dev` namespace。所有只读观测、热修、回滚和复核必须通过 UniDesk route `G14:k3s` 进入 G14 原生 k3s，并先确认目标 namespace 与节点：

```sh
tran G14:k3s kubectl get nodes -o wide
tran G14:k3s kubectl get ns hwlab-dev hwlab-prod
```

结果必须显示 G14 原生 k3s 中存在 `hwlab-dev`；需要验证 PROD 发布面时也必须显示 `hwlab-prod`。出现 D601 kubeconfig、Docker Desktop Kubernetes、`desktop-control-plane`、`127.0.0.1:11700` 或第二套 `hwlab-dev` 控制面时，停止 hotfix、回滚和验收判断。此边界与 [dev-runtime-boundary.md](dev-runtime-boundary.md) 和 [node-gitops-cicd.md](node-gitops-cicd.md) 保持一致。

## kubectl 热修命令形状

运行面热修应优先使用小补丁、短连接和远端临时文件，避免把大对象穿过 shell argv、CLI 输出截断或 `kubectl apply` 的 last-applied annotation。

推荐的 ConfigMap data 局部覆盖方式是：在本地生成只包含本次 key 的 merge patch，通过 G14 短连接写入远端临时文件，再让 `kubectl` 从真实文件读取。示例形状必须保持短连接；具体目标 ConfigMap、Deployment 和 key 按当次 issue 填写：

```sh
tran G14:k3s script <<'SCRIPT'
set -e
kubectl get ns hwlab-dev >/dev/null
patch_file=/tmp/hwlab-hotfix.patch.json
cat > "$patch_file" <<'PATCH_JSON'
{
  "metadata": {
    "annotations": {
      "hwlab.pikastech.com/hotfix-reason": "<reason>"
    }
  },
  "data": {
    "<key>": "<small-hotfix-content-or-pre-rendered-json-string>"
  }
}
PATCH_JSON
kubectl -n hwlab-dev patch configmap <hotfix-configmap> --type merge --patch-file "$patch_file"
rm -f "$patch_file"
kubectl -n hwlab-dev rollout restart deployment/<deployment>
kubectl -n hwlab-dev rollout status deployment/<deployment> --timeout=180s
SCRIPT
```

固定避坑规则：

- 不要用 `kubectl patch --patch-file -` 假设 stdin 可用；目标 `kubectl` 版本可能把 `-` 当普通文件名。用 `/tmp/*.patch.json` 再 `--patch-file`。
- 不要把大 JSON patch 放进 `kubectl patch -p "$patch"` 或 shell argv；ConfigMap 覆盖源码文件时很容易触发 `Argument list too long`。用 stdin 写远端临时文件。
- 不要把多层脚本内联穿过 `ssh -> bash -lc -> kubectl exec -> sh -lc -> node/python/powershell`。这种路径会让 heredoc、`$!`、反引号、JSON 字符串、PowerShell 管道和中文内容被不同 shell 多次解释，常见结果是实验命令还没进入 pod 就被截断或改写。
- 可靠做法是把实验逻辑沉淀为 repo-owned 脚本，或先写入远端 `/tmp/*.mjs|*.sh|*.ps1`，再用 `kubectl cp`/ConfigMap 挂载到 pod，最后用 `kubectl exec -- node /tmp/script.mjs --arg value` 或等价 argv 形态执行。只允许在外层 shell 保留短命令和固定参数，不在命令行里嵌大段 JS/PowerShell/heredoc。
- 需要从 pod 内验证同一份实验时，优先复用同一个脚本在 G14、pod 和 CI 中运行；不要把探测逻辑在每个通道手写一遍。若必须临时写脚本，文件名包含目标和 commit，issue 中记录脚本路径、commit、命令和输出摘要。
- pod 内部、G14 host、FRP 公网入口可能使用不同端口语义。`127.0.0.1:6667` 是 HWLAB 内部 cloud-api 常用端口，但 `6667` 属于 WHATWG bad port；Node/undici `fetch` 会在发包前直接报 `bad port`，包括临时 gateway 自己 poll `http://127.0.0.1:6667` 的场景。运行面实验脚本和 gateway 传输层必须用 Node `http/https` 原生 request，或改走公网 `http://74.48.78.17:17667` / service mesh 中不被 bad-port 拦截的入口。
- 遇到 `fetch failed`、`bad port`、公网通而 pod loopback 不通时，先判断是不是实验工具栈限制，不要立即归因 cloud-api、FRP、k3s Service 或 gateway 离线。issue 复盘要记录具体 URL、调用库、错误字符串和替代入口。
- 清理临时进程时不要用会匹配到清理脚本自身 `cmdline` 的宽泛字符串，例如 `hwlab-gateway-hotfix`；`node -e` 本身会把该字符串放进 `/proc/<pid>/cmdline`。清理脚本应匹配真实入口和参数组合，并显式排除 `process.pid`，或由启动脚本写 pidfile 后按 pidfile 清理。
- 正式 smoke/check 不在 master server 上执行。master server 只负责源码编辑、Git、日志和指挥；仓库级 `check`、`node --test`、browser/layout smoke、`web/hwlab-cloud-web/scripts/check.mjs` 等正式验证必须改在 G14 `/root/hwlab`、G14 k3s/Tekton、repo-owned CI 或其他获批执行面运行。
- 验证 gateway 非阻塞或长命令并发时，不要把同一路 wrapper 的长轮询 `job-status`/状态查询当作唯一证据；忙碌 gateway 可能把后续短状态查询和只读调用一起排队，导致“状态检查卡住”掩盖真实根因。优先使用 repo-owned probe：保持一个慢命令在飞，再提交一个短命令测量 quick path 延迟；完成态判断优先读 job state/log/artifact 时间戳或 quick path 结果，而不是重复发同一路长轮询。
- 不要对已经很大的 hotfix ConfigMap 使用 `kubectl apply -f -`；`apply` 会尝试写入 `kubectl.kubernetes.io/last-applied-configuration`，可能超过 Kubernetes annotation 256 KiB 限制。局部更新用 `patch --type merge --patch-file`。
- 多文件运行面覆盖第一次创建 ConfigMap 时也不要默认用 `kubectl apply -f -`；如果 data 可能超过 annotation 限制，先 `kubectl delete configmap <name> --ignore-not-found`，再 `kubectl create configmap <name> --from-file=...`，随后用 Deployment annotation 记录 hotfix 版本并 rollout。正式 CD 收口时必须删除这些 unmanaged ConfigMap/volumeMount，不让热修覆盖继续漂在运行面。
- 不要通过会截断 stdout 的控制 CLI 把 `kubectl get configmap -o json` 回读到本地再 `replace`；大 data 可能被日志/截断污染成非法 JSON。只有在确认完整无截断的原生通道中，才允许对完整对象做 `kubectl replace -f -`。
- 每次 `rollout restart` 后立即执行 `rollout status`，再用 running pod 做最小只读验证，例如 `node --check`、`grep` marker、`curl` health 或公开入口 trace/result 轮询。
- 临时补丁文件必须放在远端 `/tmp`，命名包含 hotfix 目标，命令结束后删除；issue 里只记录 ConfigMap 名、key、mountPath、operationId/traceId 和验证方法，不粘贴完整 data。

## 必须记录

每次 DEV hotfix 至少记录以下字段，写入 PR/issue/comment，不写入仓库 `reports/**`：

| 字段 | 说明 |
| --- | --- |
| Deployment | namespace、name、generation、template annotation、容器 image。 |
| Pod | 新旧 pod 名、phase、node、用于验证的 running pod。 |
| ConfigMap | 名称、key 名、用途；不得把 data 全量贴入 issue。 |
| annotation | 记录热修来源和目的的 Deployment template annotation。 |
| mount/subPath | volume 名、ConfigMap 名、mountPath、subPath、readOnly。 |
| traceId / operationId | 能力验证请求的 trace 与 operation 标识。 |
| 验证命令 | `grep` marker、`node --check`、API 返回字段等只读或最小验证命令。 |
| 回滚方式 | 移除 volume/mount/annotation 的方法，或用源码 artifact/CD 覆盖的方法。 |
| 源码化 follow-up | 关联 issue/PR，说明热修不会作为长期 runtime truth。 |

#465 的典型对象是：

| 项 | 值 |
| --- | --- |
| Deployment | `hwlab-dev/hwlab-cloud-api` |
| ConfigMap | `hwlab-cloud-api-code-agent-hotfix` |
| 覆盖文件 | `/app/internal/cloud/code-agent-chat.ts` |
| subPath | `code-agent-chat.mjs` |
| marker | `runtime-hotfix-pc-gateway-shell` |
| annotation | `hwlab.pikastech.local/pc-gateway-shell-hotfix` |
| 能力路径 | `/v1/agent/chat -> /v1/rpc/hardware.invoke.shell -> PC gateway -> Windows cmd` |

## 只读审计入口

仓库内只读入口是：

```sh
node scripts/dev-runtime-hotfix-audit.mjs --pretty
node scripts/dev-runtime-hotfix-audit.mjs --collect-readonly --pretty
```

默认 `--plan` 模式只输出 compact JSON 计划，不执行 `kubectl`。`--collect-readonly` 只允许 `kubectl get` 和 `kubectl exec` 形态的读检查，用于判断当前 DEV 是否仍被 hotfix 覆盖；它不会执行 `apply`、`patch`、`rollout`、`restart`、`delete`、`create`、`set image`，也不会读取 Secret resources 或 secret values。

输出分类固定包含以下语义：

| 分类 | 含义 |
| --- | --- |
| `hotfix-configmap-present` | 目标 ConfigMap 仍存在。 |
| `deployment-mounts-hotfix` | Deployment template 仍通过 volumeMount/subPath 覆盖目标文件。 |
| `pod-loads-hotfix-marker` | running pod 内目标文件仍包含 hotfix marker。 |
| `source-artifact-expected` | 正式收口应回到 source/PR/artifact/CD。 |
| `rollback-required` | 当前仍检测到热修残留或覆盖，不能把运行态当源码真相。 |
| `no-hotfix-detected` | 只读审计未发现该 hotfix ConfigMap 或 mount 覆盖。 |
| `unknown-needs-manual-readonly-check` | 无法确认节点、对象或 pod 状态，需要人工继续只读核对。 |

## 回滚口径

优先回滚方式是用源码化 artifact/CD 覆盖 runtime hotfix：#460/#461 合并并发布后，Deployment 应消费正式镜像，不再通过 ConfigMap 覆盖 `/app/internal/cloud/code-agent-chat.ts`。注意 `kubectl apply -k` 可能保留运行面 patch 写入、且源码 desired-state 不拥有的 hotfix `volumes`、`volumeMounts` 或 template annotations；正式 DEV CD apply 应先识别这种 unmanaged hotfix 覆盖，删除对应 desired Deployment，再由同一次 apply 从源码重新创建。

只读确认口径是通过当前 issue/CLI 明确 node + lane 的 k3s route 读取目标对象；不要从 master server 裸跑 `kubectl`，也不要把其他 node kubeconfig 当作当前控制面：

```sh
bun scripts/cli.ts ssh <node>:k3s kubectl get nodes -o jsonpath='{.items[*].metadata.name}'
bun scripts/cli.ts ssh <node>:k3s kubectl -n <namespace> get deployment hwlab-cloud-api -o json
bun scripts/cli.ts ssh <node>:k3s kubectl -n <namespace> get configmap hwlab-cloud-api-code-agent-hotfix -o json
node scripts/dev-runtime-hotfix-audit.mjs --collect-readonly --pretty
```

如果必须直接移除运行态覆盖，授权操作者应先从目标 namespace 的 `hwlab-cloud-api` Deployment JSON 中定位精确 indexes，再移除：

- 指向 hotfix ConfigMap 的 `spec.template.spec.volumes[]`；
- 指向 `/app/internal/cloud/code-agent-chat.ts` 的 `containers[].volumeMounts[]`；
- `hwlab.pikastech.local/pc-gateway-shell-hotfix` 等 hotfix annotation；
- 不再需要的 `hwlab-cloud-api-code-agent-hotfix` ConfigMap。

直接 `kubectl patch`、`kubectl delete configmap`、`kubectl rollout status` 或等价写操作必须由明确授权的操作者执行，并且当前 node/lane 运行面必须通过对应 UniDesk route 操作，例如 `D601:k3s` 或 `G14:k3s`。D601 legacy 节点确认只适用于显式旧路径事故回放，不能替代当前 node/lane hotfix 控制面。本 runbook 的审计脚本不执行这些写操作。回滚后再次运行 `scripts/dev-runtime-hotfix-audit.mjs --collect-readonly`，期望分类包含 `no-hotfix-detected`，且不包含 `deployment-mounts-hotfix`、`pod-loads-hotfix-marker` 或 `rollback-required`。

## 禁止误用

- 不得把 ConfigMap/pod 覆盖当作 source truth；正式修复必须回到 source、PR、artifact 和 repo-owned CD。
- 不得把 #462 runbook 或只读审计包装成 #460/#461 源码化完成。
- 不得把 #462 runbook 或只读审计包装成 #464 DEV-LIVE smoke/harness 完成。
- 不得把热修期间的 stdout、trace 或 operation 扩大为 M3 trusted evidence；M3 判定仍按 [m3-loop-rollout-runbook.md](m3-loop-rollout-runbook.md)。
- 不得读取或打印 Secret value、kubeconfig token、DB URL 密码、provider token。
- 不得新增或修改仓库 `reports/**`；过程证据写入 PR/issue/comment，临时 JSON 只能放 `/tmp`、`.state` 或 CI artifact。
