# Gateway 主动出站 demo

本文是 `hwlab-gateway` 最小主动出站闭环的长期参考。该 demo 只验证 gateway 主动连接 cloud、cloud 下发命令、gateway 执行并回传结果；不得把它当作 M3 `DEV-LIVE` 虚拟硬件可信闭环，也不得替代 `BOX-SIMU / Gateway-SIMU / hwlab-patch-panel` 的 M3 验收链路。

## 边界

- `hwlab-gateway` 运行在用户 PC 或本地环境，主动访问 `hwlab-cloud-api`；cloud 不需要也不应入站访问 gateway。
- demo 采用普通 HTTP poll/result：gateway 调 `POST /v1/gateway/poll` 拉取命令，执行后调 `POST /v1/gateway/result` 回传 JSON-RPC response。
- `hardware.invoke.shell` 仍是 cloud-api 对外 RPC 方法；有在线 gateway 时通过主动出站链路派发，没有在线 gateway 时保留 `not_connected` 降级返回。
- 当前命令执行能力只用于受限 demo；正式 HWPOD 控制以 [spec-hwpod-harness.md](spec-hwpod-harness.md) 和 [spec-user-access.md](spec-user-access.md) 为权威：用户权限由 `cloud-api` 的 `admin/user` 与 OpenFGA tool capability 判断；设备执行收敛到 `hwpod-cli -> hwpod-compiler-cli -> cloud-api /v1/hwpod-node-ops -> hwpod-node`。

## Cloud API 入口

- `GET /v1/gateway/sessions`：只读查看在线 gateway、队列深度和 last-seen。
- `POST /v1/gateway/poll`：gateway 主动注册、心跳并领取下一条待执行 JSON-RPC 请求。
- `POST /v1/gateway/result`：gateway 主动回传 JSON-RPC response，cloud-api 用 request id 完成等待中的 `hardware.invoke.shell`。
- `POST /json-rpc` 或 `POST /v1/rpc/hardware.invoke.shell`：用户、agent 或 smoke 仍只调用 cloud-api，不直连 gateway。

## Poll Loop 和长命令并发

Gateway 的 poll loop 不能等待单条 shell 命令结束后才继续 poll。Keil build/download、烧录或 Windows skill 可能运行数分钟；这些长命令必须在 gateway 内作为 in-flight 请求后台执行，poll loop 继续注册心跳并领取短状态读取、日志读取和取消/诊断请求。

判断问题类型时区分两种 timeout：

- `shellExecuted=true` 且有 `operationId`：命令已经到达 gateway，本次 shell 超过了命令超时；下一步应读取 job state、日志或产物时间戳，而不是盲目把超时继续调大。
- `shellExecuted=false`、`dispatchStatus=timed_out` 且旧链路可能 `operationId=null`：cloud-api 等不到 gateway 领取或回传，通常是 gateway poll loop 被前一个长命令队头阻塞，或 gateway 离线；修复重点是非阻塞 poll 和 in-flight 可观测性，不是只加大 dispatch timeout。

Gateway 必须在 registration payload 和 `/v1/gateway/sessions` 中暴露 `inflightCount`、`maxInflightRequests` 和当前 in-flight 摘要。默认允许少量并发，使一个长 Keil 操作不会阻塞后续只读 `job-status`、state/log 读取或健康探测；超过并发上限时应返回结构化 `gateway_busy`，不能让请求静默排队到 cloud dispatch timeout。

## Gateway 环境变量

| 变量 | 作用 |
| --- | --- |
| `HWLAB_GATEWAY_CLOUD_URL` | cloud-api 或 edge-proxy 地址；本地可用 `http://127.0.0.1:6667`，当前 G14 DEV 可用 `http://74.48.78.17:17667`。 |
| `HWLAB_GATEWAY_ID` | gateway 稳定身份，例如 `gtw_windows_1`。 |
| `HWLAB_GATEWAY_SESSION_ID` | gateway session id；不填时默认为 `gws_${HWLAB_GATEWAY_ID}`。 |
| `HWLAB_GATEWAY_CMD_EXEC_ENABLED=1` | 允许执行 shell 命令；未设置时 gateway 拒绝 `hardware.invoke.shell`。 |
| `HWLAB_GATEWAY_DEMO_OPEN=1` | demo 明确打开标记；本地 smoke 会设置。 |
| `HWLAB_GATEWAY_POLL_INTERVAL_MS` | poll 间隔，默认 500ms。 |
| `HWLAB_GATEWAY_MAX_INFLIGHT` | 同一 gateway 同时执行的 cloud 请求数，默认 3；用于避免长 Keil/UV4 命令阻塞短状态查询。 |
| `HWLAB_GATEWAY_CMD_TIMEOUT_MS` | 单条命令超时，默认 120000ms；Workbench 发起 Code Agent 请求时可通过“Gateway 命令超时”控件把本轮 wrapper `--timeout-ms` 调到 1/2/3/5/10 分钟。 |
| `HWLAB_GATEWAY_CMD_OUTPUT_LIMIT_BYTES` | stdout/stderr 单路输出上限，默认 65536 bytes。 |

本地或内网存在代理时，必须显式设置：

```powershell
$env:NO_PROXY="localhost,127.0.0.1,::1"
$env:no_proxy="localhost,127.0.0.1,::1"
```

## 本地验证

优先使用一键 smoke，避免手动多终端状态不一致：

```powershell
npm run gateway:demo:smoke
npm run gateway:demo:edge-smoke
```

- `gateway:demo:smoke` 启动本地 `hwlab-cloud-api` 和 `hwlab-gateway`，验证 `hardware.invoke.shell` 返回 `stdout=hwlab-demo`。
- `gateway:demo:edge-smoke` 额外启动本地 `hwlab-edge-proxy`，验证普通 HTTP proxy 能转发 `/v1/gateway/poll`、`/v1/gateway/result` 和 `/json-rpc`。
- 两个 smoke 都会设置 `NO_PROXY/no_proxy`，用于规避本地代理误触发。
- 对 G14 pod 内部 `127.0.0.1:6667` 或 service `:6667` 做 Node 探测时，不要用 Web/undici `fetch`；`6667` 属于 WHATWG bad port，Node 会直接报 `bad port`。gateway 传输层和 repo-owned 探测脚本应使用 `http/https` 原生 request，公网 `:17667` 或浏览器入口不受这个限制。

## DEV 使用

DEV cloud-api 部署包含 `/v1/gateway/*` 后，Windows gateway 可用以下方式主动连接：

```powershell
$env:HWLAB_GATEWAY_CLOUD_URL="http://74.48.78.17:17667"
$env:HWLAB_GATEWAY_ID="gtw_windows_1"
$env:HWLAB_GATEWAY_SESSION_ID="gws_gtw_windows_1"
$env:HWLAB_GATEWAY_CMD_EXEC_ENABLED="1"
$env:HWLAB_GATEWAY_DEMO_OPEN="1"
bun run .\cmd\hwlab-gateway\main.ts
```

验证时从 cloud-api 侧调用 `hardware.invoke.shell`，不要尝试从 cloud 入站访问 Windows gateway。若 `GET /v1/gateway/sessions` 能看到对应 `gatewaySessionId` 且 JSON-RPC 返回 `dispatch.shellExecuted=true`，说明主动出站 demo 链路成立。

## 后续替换为 WebSocket

HTTP poll 是最小 demo 形态。后续替换成 WebSocket 时，应保留：

- `hardware.invoke.shell` 对外 RPC 方法名；
- JSON-RPC request/response envelope；
- gateway 执行器的 stdout/stderr/exitCode/timedOut 返回形态；
- audit/evidence 由 gateway/cloud 硬件通道产生的原则；
- cloud-web、agent 和 CLI 不直连 gateway 的边界。

只替换传输层：`/v1/gateway/poll` 和 `/v1/gateway/result` 合并到 gateway 主动建立的 `/v1/gateway/ws` 长连接。
