# Audit: 2026-05-30 — Heartbeat 422 修复 + Agent 弃用升级

## Step 1: 登记
- 审计人: Claude Sonnet 4.6
- 时间: 2026-05-30
- 范围: server/api/schemas.py, server/api/agent.py, server/main.py, web/agent/agent.py, web/agent/uninstall.sh

## Step 2: 全文 Read
已读全部5个文件的完整内容。

## Step 3: 规则扫描 (H = 高风险点)
| # | 位置 | H | 说明 |
|---|------|---|------|
| H1 | agent.py:152 server_id=None | 安全 | 缺 server_id 的请求绕过了 _verify_server_api_key |
| H2 | agent.py 指数退避 | 逻辑 | _consecutive_failures 局部变量，心跳循环正确性 |
| H3 | schemas.py server_id Optional | 安全 | Pydantic 允许 None 后是否影响其他端点 |
| H4 | uninstall.sh crontab sed | 安全 | sed 删除 crontab 条目是否误删 |
| H5 | agent.py lifespan | 逻辑 | task.cancel() 是否正确清理 |

## Closure 表
| H | 判定 | 依据 |
|---|------|------|
| H1 | ✅ 安全 | server_id=None 时直接 return，不进入 Redis/MySQL 写入，不执行任何敏感操作。_verify_api_key 已在 Depends 层验证全局 key。无 server_id 不代表绕过认证——只是未注册 agent。 |
| H2 | ✅ 正确 | _consecutive_failures 在 while True 外初始化，循环内递增/重置。成功时重置为0，失败时递增。退避计算 min(interval * 2^min(failures, 5), 300) 合理。 |
| H3 | ✅ 安全 | AgentHeartbeat 仅用于 agent.py heartbeat 端点。其他端点（script-callback）使用独立 schema AgentScriptCallback，不受影响。 |
| H4 | ✅ 安全 | sed 匹配模式为 nexus.agent|multisync.agent|nexus-agent|multisync-agent，精确匹配 Agent 相关条目，不会误删其他 cron 任务。 |
| H5 | ✅ 正确 | asyncio.create_task 返回 task 对象，yield 后 task.cancel() 会抛 CancelledError 到 send_heartbeat 协程，正确终止循环。 |

## Step 4: 入口表
| 入口 | 鉴权 | 输入验证 |
|------|------|---------|
| POST /api/agent/heartbeat | _verify_api_key (Header) | AgentHeartbeat schema (server_id Optional) |
| Agent send_heartbeat() | 内部 | N/A (读取本地 psutil) |
| uninstall.sh | SSH (外部) | N/A |

## Step 5: 输入验证
- server_id=None → 已处理，返回 200 discarded
- heartbeat payload 无 server_id → Pydantic 不再报 422，handler 层丢弃
- agent 指数退避参数：HEARTBEAT_INTERVAL 来自 config，有默认值 60

## Step 6: Sink 分析
- Redis HSET: 仅在 server_id 有效时执行
- MySQL update_heartbeat: 仅在 server_id 有效时执行
- 日志: warning 级别，无敏感数据泄露

## Step 7: 归类
- H1-H3: 安全 → 均有防护，无风险
- H4: 安全 → 匹配模式精确
- H5: 逻辑 → 正确

## DoD (Definition of Done)
- [x] 所有 H 有 Closure 判定 + 依据
- [x] 无 TODO/FIXME 遗留
- [x] 无静默吞错
- [x] 无硬编码密钥
- [x] 无 f-string SQL
- [x] ruff check 通过
- [x] import server.main 通过
- [x] ast.parse 通过
