From 4fe412eac30227f654b607bd8138aca4d63820c9 Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Thu, 1 Oct 2026 10:09:13 +0000 Subject: [PATCH] =?UTF-8?q?fix(runtime):=20ROUTER=5FBASE=5FURL=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=20=D1=87=D0=B8=D1=82=D0=B0=D1=8E=D1=82?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B8=D0=B7=20.env=20(e-261001091912)=20+=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit systemd-юниты не экспортируют .env; на Hetzner-агентах base_url тихо уходил в дефолт 127.0.0.1:20128 (живой 9router только на эталонном хосте). _env_or_file(): env > .env > default. E2E на агенте 82: venv без openai (в cloud-init добавлен отдельно в coop). --- harness/runtime.py | 35 ++++++++++----- tests/test_router_env_fallback.py | 74 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 tests/test_router_env_fallback.py diff --git a/harness/runtime.py b/harness/runtime.py index cf39b5e..2542afb 100644 --- a/harness/runtime.py +++ b/harness/runtime.py @@ -11,20 +11,35 @@ from datetime import datetime, timezone _HARNESS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +def _env_or_file(name: str, default: str = "") -> str: + """env → файл .env рядом с кодом → default. + + systemd-юниты харнеса не экспортируют .env (только PYTHONUNBUFFERED, + t-261001091912-24602f): на агентских серверах .env читается построчно + отсюда. Env имеет приоритет (12-фактор), файл — оффлайн-фолбэк. + """ + val = os.environ.get(name, "") + if val: + return val + path = os.path.join(_HARNESS_DIR, ".env") + try: + with open(path) as f: + for line in f: + if line.startswith(name + "="): + return line.split("=", 1)[1].strip() + except OSError: + pass + return default + + def _router_key(): - env = os.environ.get("ROUTER_KEY", "") - if not env: - path = os.path.join(_HARNESS_DIR, ".env") - if os.path.exists(path): - for line in open(path): - if line.startswith("ROUTER_KEY="): - env = line.split("=", 1)[1].strip() - break - return env + return _env_or_file("ROUTER_KEY") def _router_base_url(): - return os.environ.get("ROUTER_BASE_URL", "http://127.0.0.1:20128/v1") + # Фолбэк на .env обязателен для агентов вне эталонного хоста: дефолт + # 127.0.0.1:20128 живёт только там, где стоит 9router локально. + return _env_or_file("ROUTER_BASE_URL", "http://127.0.0.1:20128/v1") def build_level_model(level_cfg): diff --git a/tests/test_router_env_fallback.py b/tests/test_router_env_fallback.py new file mode 100644 index 0000000..d034854 --- /dev/null +++ b/tests/test_router_env_fallback.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""t-261001091912-24602f: ROUTER_BASE_URL/ключ берётся и из .env, не только из env. + +Запуск: ./venv/bin/python tests/test_router_env_fallback.py + +Баг в e2e: systemd-юниты харнеса не экспортируют .env, runtime.py читал +ROUTER_BASE_URL только из окружения → у свежеразвёрнутого агента +base_url тихо уходил в дефолт 127.0.0.1:20128 (на эталонном хосте так +жив 9router, на Hetzner-агенте — нет) и LLM-тики падали до первого +look в .env. Фикс: _env_or_file() — env имеет приоритет, .env рядом +с кодом — фолбэк (как раньше делал _router_key); EnvironmentFile в +юнитах cloud-init — страховка сверху. +""" +import os +import sys +import tempfile + +sys.path.insert(0, "/root/simbiot-harness") + +fails = [] + + +def check(name, cond, extra=""): + print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "") + if not cond: + fails.append(name) + + +from harness import runtime + +# hermetic: окружение тест-процесса не должно подсказывать значения +for _v in ("ROUTER_BASE_URL", "ROUTER_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL"): + os.environ.pop(_v, None) + +# временный "каталог харнеса" с фейковым .env — реальный не трогаем +tmpdir = tempfile.mkdtemp(prefix="simbiot-envtest-") +with open(os.path.join(tmpdir, ".env"), "w") as f: + f.write("ROUTER_BASE_URL=https://router.cil.by/v1\n") + f.write("ROUTER_KEY=sk-envtest-key\n") + f.write("# комментарий\n") + +runtime._HARNESS_DIR = tmpdir + +# 1. env отсутствует, .env есть → значение из .env (суть фикса) +check("1: base_url из .env quando env пуст", + runtime._router_base_url() == "https://router.cil.by/v1", + str(runtime._router_base_url())) +check("1: ключ из .env (прежнее поведение сохранено)", + runtime._router_key() == "sk-envtest-key") + +# 2. env присутствует → env сильнее .env +os.environ["ROUTER_BASE_URL"] = "https://env.takes/priority/v1" +check("2: env приоритетнее .env", + runtime._router_base_url() == "https://env.takes/priority/v1") +os.environ.pop("ROUTER_BASE_URL") + +# 3. нет ни env, ни .env → дефолт с локальным роутером +runtime._HARNESS_DIR = tempfile.mkdtemp(prefix="simbiot-envtest-empty-") +check("3: нет .env → дефолт 127.0.0.1:20128", + runtime._router_base_url() == "http://127.0.0.1:20128/v1", + str(runtime._router_base_url())) + +# 4. setup_provider(): end-to-end маппинг .env → OPENAI_* env +runtime._HARNESS_DIR = tmpdir +runtime.setup_provider() +check("4: setup_provider кладёт OPENAI_BASE_URL из .env", + os.environ["OPENAI_BASE_URL"] == "https://router.cil.by/v1", + os.environ.get("OPENAI_BASE_URL", "")) +check("4: setup_provider кладёт OPENAI_API_KEY из .env", + os.environ.get("OPENAI_API_KEY") == "sk-envtest-key") + +print() +print("FAILS:", len(fails), "| файл:", " | ".join(fails) if fails else "—") +sys.exit(1 if fails else 0) \ No newline at end of file