de8d860244
- AgentHeartbeat.server_id changed to Optional[int] — missing server_id
now returns 200 (discarded) instead of 422, stopping retry loops
- web/agent/agent.py: on_event("startup") → FastAPI lifespan pattern
- Added exponential backoff on consecutive heartbeat failures (cap 5min)
- Agent stops heartbeat loop on "discarded" response from central
- main.py: promoted temporary debug 422 handler to production-grade
- uninstall.sh: added legacy agent cleanup (pkill patterns, crontab,
/opt/multisync-agent path)
- Confirmed servers.html batch buttons work correctly (disabled state
is correct when no agents installed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2.9 KiB
2.9 KiB
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 |
| 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)
- 所有 H 有 Closure 判定 + 依据
- 无 TODO/FIXME 遗留
- 无静默吞错
- 无硬编码密钥
- 无 f-string SQL
- ruff check 通过
- import server.main 通过
- ast.parse 通过