# BL-06：强制 Agent per-server key（移除 global 回退）

**日期**：2026-06-07  
**动机**：生产 0 台子机窗口，关闭 global `API_KEY` 对 Agent 心跳/回调的 legacy 回退，消除「伪造未迁移服务器心跳」残余面；收口 `risk-acceptance-single-operator.md` 接受项 2。

## 变更摘要

- Agent 认证（`/api/agent/heartbeat`、`/api/agent/script-callback`）仅接受 per-server `agent_api_key`
- 巡检修复：`_verify_server_api_key` 返回 `Server` 对象，心跳路径合并为单次 `get_server`（原重复查询）
- 安装链路（单台/批量 `install-agent`）无 per-server key 时明确报错，不再静默使用 global key
- 长任务回调注册（`script_service._agent_api_key_for_server`）同样要求 per-server key

## 涉及文件

| 文件 | 改动 |
|------|------|
| `server/api/agent.py` | `_verify_api_key`、`_verify_server_api_key` 移除 global 回退 |
| `server/api/servers.py` | `install_agent_remote`、`batch_install_agent` 无 key 报错 |
| `server/application/services/script_service.py` | `_agent_api_key_for_server` 无 key 抛 `ValueError` |
| `tests/test_agent_per_server_key.py` | 新增回归测试 |
| `tests/load_test.py`、`tests/quick_load.py` | 注释说明 heartbeat 需 per-server key |
| `docs/project/risk-acceptance-single-operator.md` | 接受项 2 标记已收口 |
| `docs/reports/2026-06-06-external-attack-hardening-backlog.md` | BL-06 已实施 |
| `docs/reports/2026-06-06-external-attack-surface-audit.md` | §7 状态更新 |

## 运维语义

- 新建服务器（单建/CSV/批量导入）仍自动生成 `nxs-...` per-server key
- `uninstall-agent` 将 `agent_api_key` 置 `None`；重装前须 `POST /api/servers/{id}/agent-key`
- global `API_KEY` 仍为必需环境变量（启动校验等），但**不再**用于 Agent 认证

## 迁移 / 重启

- 无需 DB 迁移
- 需重启 API 容器使代码生效

## 验证

```bash
pytest tests/test_agent_per_server_key.py tests/test_external_attack_surface_hardening.py tests/test_install_locked_404.py -q
bash deploy/pre_deploy_check.sh
```

回归：`test_verify_server_key_single_get_server_per_auth_check` 断言认证路径 `get_server` 仅调用一次。

生产探测：`POST /api/agent/heartbeat` 带 global `API_KEY` + 任意 `server_id` → 期望 **401**。

## 回滚

恢复 `agent.py` global 回退分支及 `servers.py` / `script_service.py` 中 `or settings.API_KEY` 即可。
