From 972456a246fb1bda9ccda1ce68a919e6ca17ce2b Mon Sep 17 00:00:00 2001 From: Nexus Agent Date: Mon, 8 Jun 2026 23:05:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20401=20=E5=81=9C=E5=BF=83?= =?UTF-8?q?=E8=B7=B3=E3=80=81=E9=85=8D=E7=BD=AE=E9=87=8D=E8=BD=BD=E5=90=8E?= =?UTF-8?q?=E9=87=8D=E5=90=AF=E4=B8=8E=E5=8D=87=E7=BA=A7=E5=8F=8C=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8B=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提取 heartbeat_policy;install/upgrade 同步 heartbeat_policy.py;config reload 恢复心跳协程。 Co-authored-by: Cursor --- .../2026-06-08-agent-401-stop-heartbeat.md | 26 ++++++++ .../2026-06-08-agent-401-stop-heartbeat.md | 40 +++++++++++++ .../2026-06-08-agent-401-stop-heartbeat.md | 30 ++++++++++ docs/project/AI-HANDOFF-2026-06-08.md | 30 ++++------ server/api/servers.py | 5 +- server/application/server_batch_common.py | 15 ++++- .../services/server_batch_service.py | 4 +- server/utils/agent_version.py | 8 +-- tests/test_agent_heartbeat_stop.py | 36 +++++++++++ web/agent/agent.py | 59 +++++++++++++------ web/agent/heartbeat_policy.py | 18 ++++++ web/agent/install.sh | 8 ++- 12 files changed, 232 insertions(+), 47 deletions(-) create mode 100644 docs/audit/2026-06-08-agent-401-stop-heartbeat.md create mode 100644 docs/changelog/2026-06-08-agent-401-stop-heartbeat.md create mode 100644 docs/design/plans/2026-06-08-agent-401-stop-heartbeat.md create mode 100644 tests/test_agent_heartbeat_stop.py create mode 100644 web/agent/heartbeat_policy.py diff --git a/docs/audit/2026-06-08-agent-401-stop-heartbeat.md b/docs/audit/2026-06-08-agent-401-stop-heartbeat.md new file mode 100644 index 00000000..573b22a4 --- /dev/null +++ b/docs/audit/2026-06-08-agent-401-stop-heartbeat.md @@ -0,0 +1,26 @@ +# 审计 — Agent 401 停心跳(A-05) + +**Changelog**: `docs/changelog/2026-06-08-agent-401-stop-heartbeat.md` + +## 变更文件 + +`heartbeat_policy.py` · `agent.py` · `install.sh` · `servers.py` · `server_batch_common.py` · `server_batch_service.py` · `agent_version.py` · `test_agent_heartbeat_stop.py` + +## Step 3 + +| 项 | 结论 | +|----|------| +| 401 / discarded 后 `return` 退出循环 | PASS | +| 5xx 仍退避重试,非静默吞错 | PASS | +| config reload 重启已 done 的心跳任务 | PASS | +| 子机 rollout 依赖 upgrade-agent(文档已写明) | PASS | + +## Closure + +Gate 7/7 PASS · 待部署 + 子机批量升级验证 + +## DoD + +- [x] `heartbeat_should_stop` 单测 +- [x] `get_central_agent_version` 仍读 2.0.0 +- [ ] 生产抽样子机 journalctl 401 后无重复心跳(#7 rollout 后) diff --git a/docs/changelog/2026-06-08-agent-401-stop-heartbeat.md b/docs/changelog/2026-06-08-agent-401-stop-heartbeat.md new file mode 100644 index 00000000..8c15057c --- /dev/null +++ b/docs/changelog/2026-06-08-agent-401-stop-heartbeat.md @@ -0,0 +1,40 @@ +# 2026-06-08 — Agent 401 停心跳 + 配置重载后重启(A-05) + +## 摘要 + +边端 Agent 心跳收到中心 **401** 或 **discarded** 后永久退出循环;`/config/reload` 写入新 key 后自动重启心跳任务。策略提取为 `heartbeat_policy.py` 并加单测。 + +## 动机 + +错误 API key 时旧 Agent 每 60s 无限重试;401 停循环已在 `72d82d7` 落地,但缺单测、缺 key 修复后自动恢复、子机文件未统一 rollout。 + +## 涉及文件 + +- `web/agent/heartbeat_policy.py`(新) +- `web/agent/agent.py` — `AGENT_VERSION` 常量、config reload 重启心跳 +- `web/agent/install.sh` — 同步下载 `heartbeat_policy.py` +- `server/application/server_batch_common.py` — `agent_files_curl_cmds` 升级双文件 +- `server/api/servers.py` · `server_batch_service.py` — 升级拉取双文件 +- `server/utils/agent_version.py` — 读 `AGENT_VERSION` 常量 +- `tests/test_agent_heartbeat_stop.py` + +## 迁移 / 重启 + +- 中心:`/agent/agent.py` 随 API 镜像更新 +- **各子机**:须批量 **升级 Agent** 或重装,否则仍跑旧 `agent.py`(见 backlog #7) + +## 行为 + +| 中心响应 | Agent | +|----------|--------| +| 401 Invalid API key | 停止心跳,打 error 日志 | +| 200 `status=discarded` | 停止心跳(错误 server_id 等) | +| 5xx / 网络错误 | 指数退避重试(不变) | +| `/config/reload` 新 key | 若心跳已停,重启协程 | + +## 验证 + +```bash +pytest tests/test_agent_heartbeat_stop.py tests/test_agent_version.py -q +bash scripts/local_verify.sh +``` diff --git a/docs/design/plans/2026-06-08-agent-401-stop-heartbeat.md b/docs/design/plans/2026-06-08-agent-401-stop-heartbeat.md new file mode 100644 index 00000000..93f184b2 --- /dev/null +++ b/docs/design/plans/2026-06-08-agent-401-stop-heartbeat.md @@ -0,0 +1,30 @@ +# 2026-06-08 — Agent 401 停心跳(A-05) + +## 背景 + +BL-06 后中心仅接受 per-server `agent_api_key`;错误 key 时心跳 401。边端 Agent 须停止 `while True` 重试,避免日志与无效流量。`72d82d7` 已加 401 `return`,本子任务补齐可测策略、配置重载后重启心跳、子机 rollout 说明。 + +## 方案 + +1. `web/agent/heartbeat_policy.py` — `heartbeat_should_stop(status_code, body)` 纯函数 +2. `agent.py` — 401 / discarded 统一走策略;`AGENT_VERSION` 常量 SSOT +3. `/config/reload` 更新 key 后 `_start_heartbeat_task()` 重启已停止的心跳协程 +4. 子机需 **批量升级 Agent** 拉取含本逻辑的 `agent.py`(backlog #7) + +## 涉及文件 + +- `web/agent/heartbeat_policy.py` +- `web/agent/agent.py` +- `server/utils/agent_version.py` — 读 `AGENT_VERSION` 常量 +- `tests/test_agent_heartbeat_stop.py` + +## 验证 + +```bash +pytest tests/test_agent_heartbeat_stop.py tests/test_agent_version.py -q +# 子机:故意错 key → journalctl 一条 401 后无新心跳;修正 key + config reload 后恢复 +``` + +## 回滚 + +恢复 agent.py 内联 401 分支;删除 heartbeat_policy.py diff --git a/docs/project/AI-HANDOFF-2026-06-08.md b/docs/project/AI-HANDOFF-2026-06-08.md index 88d8e809..708f7aef 100644 --- a/docs/project/AI-HANDOFF-2026-06-08.md +++ b/docs/project/AI-HANDOFF-2026-06-08.md @@ -36,17 +36,11 @@ push:bash scripts/git-push.sh(凭据 deploy/nexus-1panel.secrets.sh,gitign - 用户报「server_ids 最多 50 项s after validation, not 101」→ 已修并部署 a401ea5 - 设置页连接池保存 422「Input should be a valid string」→ 已修并部署 ff143c2/632891e -【本地未提交 · 下一会话优先】 -☐ server_batch 僵尸回收 + stuck 扫描(代码已在工作区,未 commit/部署) - - server/background/server_batch_reconcile.py(新) - - server/application/services/server_batch_service.py(改) - - server/infrastructure/database/server_batch_job_repo.py(改) - - server/infrastructure/redis/server_batch_store.py(改) - - server/main.py(改:启动 recover + 60s reconcile loop) - - tests/test_server_batch_reconcile.py(新) - - docs/changelog/2026-06-08-server-batch-reconcile.md(新) - - docs/design/plans/2026-06-08-server-batch-reconcile.md(新) - → 流程:补 audit → pytest → commit → push → deploy +【2026-06-08 已部署 69068e2】 +- server_batch 僵尸回收 + stuck 60s 扫描 +- sync_logs 30 天 purge(启动首轮 + 24h) +- 批量安装 Agent:已装且版本 ≥ 主站则跳过 +- 验证:`docs/reports/2026-06-08-full-align-deploy-verification.md` 【用户终验待确认】 - 全选 101 台批量操作(install-agent / detect-path)不再 422 @@ -55,9 +49,9 @@ push:bash scripts/git-push.sh(凭据 deploy/nexus-1panel.secrets.sh,gitign - WebSSH 实连 pwd、Telegram 测试发送、推送预览(勿误推生产) 【优先待办 TOP 10】 -1. server_batch 僵尸回收 — **本地已实现,待 commit + 部署**(见上) -2. sync_logs 30 天自动清理 ☑(本地实现,待部署) -3. Agent 401 后停循环(A-05 / web/agent/agent.py) +1. server_batch 僵尸回收 ☑(69068e2 已部署) +2. sync_logs 30 天自动清理 ☑(69068e2 已部署) +3. Agent 401 停循环 ☑(代码+单测;子机 rollout 见 #7) 4. Agent upgrade 与 install.sh venv 对齐(D-01) 5. ~75 条后端 API 英文 detail 改中文(Pydantic 422 已部分中文化) 6. 更新 AI-HANDOFF + alignment-plan 勾选状态(文档 SSOT) @@ -73,7 +67,7 @@ push:bash scripts/git-push.sh(凭据 deploy/nexus-1panel.secrets.sh,gitign 【勿重复做 / 已否决】RBAC、移动端、会话录像、审计 CSV 导出、邮件报告、Redis Cluster、独立文件编辑器 【已风险接受】WebSocket JWT query(ADR-011)、Agent global API_KEY legacy — risk-acceptance-single-operator.md -【工作区】a401ea5 已 push/deploy;本地勿提交 web/app/assets/* 构建残留、gate_log.jsonl、.cursor/settings.json +【工作区】69068e2 已 push/deploy;本地勿提交 web/app/assets/* 构建残留、gate_log.jsonl、.cursor/settings.json 【文档】Handoff 本文 · 功能指南 nexus-functional-development-guide.md · 验收 development-acceptance-standard.md ``` @@ -83,7 +77,7 @@ push:bash scripts/git-push.sh(凭据 deploy/nexus-1panel.secrets.sh,gitign | 项 | 值 | |----|-----| -| 分支 | `main` @ **`a401ea5`** | +| 分支 | `main` @ **`69068e2`** | | 生产 URL | `https://api.synaglobal.vip` | | 运行时 | Docker Compose @ `/opt/nexus`(1panel-network) | | SSH | `azureuser@20.24.218.235`(本机 `Host nexus` 常不可用,用 IP + pem) | @@ -120,7 +114,7 @@ push:bash scripts/git-push.sh(凭据 deploy/nexus-1panel.secrets.sh,gitign | # | 项 | 状态 | |---|-----|------| | 3 | sync_logs 30 天自动 purge | ☑ | -| 4 | Agent 401 停循环(A-05) | ☐ | +| 4 | Agent 401 停循环(A-05) | ☑~(待部署+子机升级) | | 5 | upgrade-agent 与 install.sh 对齐(D-01) | ☐ | | — | ~75 条后端 API 英文 `detail` 改中文 | ☐ | | — | ~25 前端英/混排标签 | ☐ | @@ -158,7 +152,7 @@ push:bash scripts/git-push.sh(凭据 deploy/nexus-1panel.secrets.sh,gitign 1. server_batch 僵尸任务回收(例 #20) ☑~ **(本地已实现,待部署)** 2. server_batch 启动 stuck 扫描 ☑~ **(同上)** 3. sync_logs 30 天自动 purge ☑ -4. Agent 401 停循环(A-05) ☐ +4. Agent 401 停循环(A-05) ☑~ 5. upgrade-agent 与 install.sh 对齐(D-01) ☐ 6. 生产复核 `push_schedules.target_path` 列(A-02) ~ 7. 各子机同步 agent.py(A-05 依赖) ☐ diff --git a/server/api/servers.py b/server/api/servers.py index 3d84c52d..16746da1 100644 --- a/server/api/servers.py +++ b/server/api/servers.py @@ -1724,7 +1724,8 @@ async def upgrade_agent( detail="NEXUS_API_BASE_URL 未配置,无法构建下载地址", ) - agent_url = f"{base_url}/agent/agent.py" + from server.application.server_batch_common import agent_files_curl_cmds + install_dir = "/opt/nexus-agent" venv_python = f"{install_dir}/.venv/bin/python" ssh_user = (server.username or "root").strip() @@ -1776,7 +1777,7 @@ async def upgrade_agent( upgrade_cmd = _sudo_wrap( f"{pip_cmd} " f"&& {backup_cmd} " - f"&& curl -fsSL {shlex.quote(agent_url)} -o {install_dir}/agent.py " + f"&& {agent_files_curl_cmds(base_url, install_dir)} " f"&& {kill_port}{systemctl_prefix}systemctl restart nexus-agent " f"&& sleep 2 " f"&& {systemctl_prefix}systemctl is-active nexus-agent " diff --git a/server/application/server_batch_common.py b/server/application/server_batch_common.py index 921e833a..efdb6bee 100644 --- a/server/application/server_batch_common.py +++ b/server/application/server_batch_common.py @@ -161,11 +161,21 @@ async def mark_agent_uninstalled(sid: int) -> None: _REMOTE_AGENT_VERSION_CMD = ( "if [ -f /opt/nexus-agent/agent.py ]; then " - "grep -m1 '\"agent_version\"' /opt/nexus-agent/agent.py 2>/dev/null | " - "sed -n 's/.*\"agent_version\": *\"\\([^\"]*\\)\".*/\\1/p'; fi" + "grep -m1 '^AGENT_VERSION' /opt/nexus-agent/agent.py 2>/dev/null | " + "sed -n 's/.*AGENT_VERSION *= *\"\\([^\"]*\\)\".*/\\1/p'; fi" ) +def agent_files_curl_cmds(base_url: str, install_dir: str = "/opt/nexus-agent") -> str: + """Download agent.py and heartbeat_policy.py from central /agent/.""" + root = base_url.rstrip("/") + return ( + f"curl -fsSL {shlex.quote(root + '/agent/agent.py')} -o {install_dir}/agent.py " + f"&& curl -fsSL {shlex.quote(root + '/agent/heartbeat_policy.py')} " + f"-o {install_dir}/heartbeat_policy.py" + ) + + async def _redis_agent_version(server_id: int) -> str: from server.infrastructure.redis.client import get_redis @@ -246,6 +256,7 @@ def result_item_dict( __all__ = [ + "agent_files_curl_cmds", "agent_install_skip_result", "batch_server_display_name", "ensure_agent_api_key", diff --git a/server/application/services/server_batch_service.py b/server/application/services/server_batch_service.py index 9ff3f15c..c4345101 100644 --- a/server/application/services/server_batch_service.py +++ b/server/application/services/server_batch_service.py @@ -8,6 +8,7 @@ import shlex from typing import Any, Optional from server.application.server_batch_common import ( + agent_files_curl_cmds, agent_install_skip_result, batch_server_display_name, ensure_agent_api_key, @@ -588,7 +589,6 @@ async def _run_upgrade_agent(live: dict[str, Any]) -> None: if not server: return result_item_dict(server_id=sid, server_name=label, success=False, error="服务器不存在") try: - agent_url = f"{base_url}/agent/agent.py" install_dir = "/opt/nexus-agent" venv_python = f"{install_dir}/.venv/bin/python" ssh_user = (server.username or "root").strip() @@ -599,7 +599,7 @@ async def _run_upgrade_agent(live: dict[str, Any]) -> None: f"&& {venv_python} -c 'import sys; v=sys.version_info; sys.exit(0 if v.major==3 and v.minor>=10 else 1)' " f"&& {venv_python} -m pip install -q fastapi==0.115.6 uvicorn==0.34.0 httpx==0.28.1 psutil python-multipart==0.0.19 " f"&& cp {install_dir}/agent.py {install_dir}/agent.py.bak " - f"&& curl -fsSL {shlex.quote(agent_url)} -o {install_dir}/agent.py " + f"&& {agent_files_curl_cmds(base_url, install_dir)} " f"&& {batch_kill_port}{batch_systemctl_prefix}systemctl restart nexus-agent " f"&& sleep 2 " f"&& {batch_systemctl_prefix}systemctl is-active nexus-agent " diff --git a/server/utils/agent_version.py b/server/utils/agent_version.py index 6ee53199..bba32c66 100644 --- a/server/utils/agent_version.py +++ b/server/utils/agent_version.py @@ -7,16 +7,16 @@ from functools import lru_cache from pathlib import Path _AGENT_PY = Path(__file__).resolve().parents[2] / "web" / "agent" / "agent.py" -_VERSION_IN_AGENT_RE = re.compile(r'"agent_version":\s*"([^"]+)"') +_AGENT_VERSION_CONST_RE = re.compile(r'^AGENT_VERSION\s*=\s*"([^"]+)"', re.MULTILINE) @lru_cache(maxsize=1) def get_central_agent_version() -> str: - """Read agent_version string shipped at /agent/agent.py (heartbeat payload).""" + """Read AGENT_VERSION constant from web/agent/agent.py (heartbeat SSOT).""" text = _AGENT_PY.read_text(encoding="utf-8") - match = _VERSION_IN_AGENT_RE.search(text) + match = _AGENT_VERSION_CONST_RE.search(text) if not match: - raise RuntimeError(f"agent_version not found in {_AGENT_PY}") + raise RuntimeError(f"AGENT_VERSION not found in {_AGENT_PY}") return match.group(1) diff --git a/tests/test_agent_heartbeat_stop.py b/tests/test_agent_heartbeat_stop.py new file mode 100644 index 00000000..552fb539 --- /dev/null +++ b/tests/test_agent_heartbeat_stop.py @@ -0,0 +1,36 @@ +"""Agent heartbeat 401 / discarded stop policy (A-05).""" + +import sys +from pathlib import Path + +import pytest + +_AGENT_DIR = Path(__file__).resolve().parents[1] / "web" / "agent" +if str(_AGENT_DIR) not in sys.path: + sys.path.insert(0, str(_AGENT_DIR)) + +from heartbeat_policy import heartbeat_should_stop # noqa: E402 + +from server.utils.agent_version import get_central_agent_version # noqa: E402 + + +def test_stop_on_401(): + assert heartbeat_should_stop(401) is True + assert heartbeat_should_stop(401, {}) is True + + +def test_stop_on_discarded(): + assert heartbeat_should_stop(200, {"status": "discarded", "reason": "missing server_id"}) is True + + +def test_continue_on_success(): + assert heartbeat_should_stop(200, {"status": "ok"}) is False + + +def test_continue_on_transient_errors(): + assert heartbeat_should_stop(500) is False + assert heartbeat_should_stop(502, None) is False + + +def test_central_agent_version_matches_constant(): + assert get_central_agent_version() == "2.0.0" diff --git a/web/agent/agent.py b/web/agent/agent.py index 883d3b62..17b1f7e3 100644 --- a/web/agent/agent.py +++ b/web/agent/agent.py @@ -16,6 +16,10 @@ from pathlib import Path from typing import Optional import httpx + +from heartbeat_policy import heartbeat_should_stop + +AGENT_VERSION = "2.0.0" import psutil from fastapi import FastAPI, HTTPException, Header, Request, Depends from fastapi.responses import JSONResponse @@ -65,15 +69,28 @@ logging.basicConfig( ) logger = logging.getLogger("nexus-agent") +_heartbeat_task: Optional[asyncio.Task] = None + # --- FastAPI App --- from contextlib import asynccontextmanager + +def _start_heartbeat_task() -> asyncio.Task: + """Start heartbeat loop (idempotent if previous task still running).""" + global _heartbeat_task + if _heartbeat_task is not None and not _heartbeat_task.done(): + return _heartbeat_task + _heartbeat_task = asyncio.create_task(send_heartbeat()) + return _heartbeat_task + + @asynccontextmanager async def agent_lifespan(app): """Application lifecycle: start heartbeat task on startup, clean up on shutdown.""" + global _heartbeat_task logger.info(f"Nexus Agent starting (server_id={SERVER_ID})") - task = asyncio.create_task(send_heartbeat()) + _start_heartbeat_task() host = config.get("server", {}).get("host", "127.0.0.1") port = config.get("server", {}).get("port", 8601) @@ -81,10 +98,12 @@ async def agent_lifespan(app): yield - task.cancel() + if _heartbeat_task is not None: + _heartbeat_task.cancel() + _heartbeat_task = None logger.info("Nexus Agent stopped") -app = FastAPI(title="Nexus Agent", version="2.0.0", lifespan=agent_lifespan) +app = FastAPI(title="Nexus Agent", version=AGENT_VERSION, lifespan=agent_lifespan) def _ip_allowed(client_ip: str) -> bool: @@ -305,6 +324,10 @@ async def reload_config(data: dict, _auth=Depends(verify_api_key)): CENTRAL_URL = config.get("central", {}).get("url", CENTRAL_URL) CENTRAL_API_KEY = config.get("central", {}).get("api_key", CENTRAL_API_KEY) + if CENTRAL_URL and SERVER_ID and CENTRAL_API_KEY: + _start_heartbeat_task() + logger.info("Heartbeat task (re)started after config reload") + return {"status": "ok", "message": "Config updated", "updated_at": now} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -377,7 +400,7 @@ async def send_heartbeat(): payload = { "server_id": SERVER_ID, "is_online": True, - "agent_version": "2.0.0", + "agent_version": AGENT_VERSION, "system_info": { "cpu_usage": cpu, "mem_usage": mem, @@ -396,26 +419,28 @@ async def send_heartbeat(): json=payload, headers={"X-API-Key": CENTRAL_API_KEY}, ) - if resp.status_code == 200: - data = resp.json() - if data.get("status") == "discarded": + body = resp.json() if resp.status_code == 200 else None + if heartbeat_should_stop(resp.status_code, body): + if resp.status_code == 401: + logger.error( + "Heartbeat rejected by central (401): invalid API key or " + "server_id=%s not configured with per-server agent_api_key. " + "Stopping heartbeat — reconfigure agent key via admin UI.", + SERVER_ID, + ) + else: + reason = (body or {}).get("reason", "unknown") logger.error( "Heartbeat discarded by central: %s. " "This agent may be misconfigured (wrong server_id=%s). " "Stopping heartbeat to prevent noise.", - data.get("reason", "unknown"), SERVER_ID, + reason, + SERVER_ID, ) - return # Exit heartbeat loop — agent needs reconfiguration + return + if resp.status_code == 200: _consecutive_failures = 0 logger.debug("Heartbeat sent") - elif resp.status_code == 401: - logger.error( - "Heartbeat rejected by central (401): invalid API key or " - "server_id=%s not configured with per-server agent_api_key. " - "Stopping heartbeat — reconfigure agent key via admin UI.", - SERVER_ID, - ) - return else: _consecutive_failures += 1 logger.warning(f"Heartbeat failed: {resp.status_code}") diff --git a/web/agent/heartbeat_policy.py b/web/agent/heartbeat_policy.py new file mode 100644 index 00000000..cd803ab9 --- /dev/null +++ b/web/agent/heartbeat_policy.py @@ -0,0 +1,18 @@ +"""Heartbeat loop stop conditions — shared by agent.py and unit tests.""" + +from __future__ import annotations + +from typing import Any, Mapping, Optional + + +def heartbeat_should_stop( + status_code: int, + body: Optional[Mapping[str, Any]] = None, +) -> bool: + """True when central rejected heartbeat permanently (do not retry).""" + if status_code == 401: + return True + if status_code == 200 and isinstance(body, Mapping): + if body.get("status") == "discarded": + return True + return False diff --git a/web/agent/install.sh b/web/agent/install.sh index 35998597..6a15ddb4 100644 --- a/web/agent/install.sh +++ b/web/agent/install.sh @@ -253,13 +253,17 @@ PIP_DEPS="fastapi==${FASTAPI_VER} uvicorn==${UVICORN_VER} httpx==${HTTPX_VER} py [ -n "$PSUTIL_VER" ] && PIP_DEPS="${PIP_DEPS} psutil==${PSUTIL_VER}" || PIP_DEPS="${PIP_DEPS} psutil" pip install -q $PIP_DEPS -# 4. Download agent.py -echo "[4/7] Download agent.py..." +# 4. Download agent.py + heartbeat_policy.py +echo "[4/7] Download agent files..." [ -z "$WEB_URL" ] && WEB_URL="${CENTRAL_URL}" curl -fsSL "${WEB_URL}/agent/agent.py" -o "$INSTALL_DIR/agent.py" || { echo "ERROR: Cannot download from $WEB_URL/agent/agent.py" exit 1 } +curl -fsSL "${WEB_URL}/agent/heartbeat_policy.py" -o "$INSTALL_DIR/heartbeat_policy.py" || { + echo "ERROR: Cannot download from $WEB_URL/agent/heartbeat_policy.py" + exit 1 +} # 5. Config echo "[5/7] Write config..."