Files
Nexus/docs/audit/2026-05-30-heartbeat-422-fix.md
T
Your Name de8d860244 fix: handle legacy agent heartbeat without server_id + migrate on_event to lifespan
- 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>
2026-05-30 18:20:14 +08:00

2.9 KiB
Raw Blame History

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 不再报 422handler 层丢弃
  • 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 通过