t-261002193628-1d1e5f: установка одной командой + визард первого запуска

- scripts/install.sh: clone → venv → deps → обёртка ~/.local/bin/simbiot
  (чистый env, как hermes) → тихий init/визард → проверка simbiot status;
  флаги --dir/--repo/--wizard/--no-wizard/--systemd/-q
- cli.py wizard: интерактивный конфиг .env (TG-токен, чаты, ключ 9router,
  модель SIMBIOT_MODEL) + init стора; --noninteractive и флаги
- harness/levels.py: SIMBIOT_MODEL переопределяет модель уровней (env/.env)
- requirements.txt: pinned-зависимости для установщика
- scripts/simbiot-harness.service: systemd-юнит (опция --systemd)
- README.md: секция «Установка одной командой»
This commit is contained in:
Simbiot Harness 2026-10-02 19:52:19 +00:00
parent e435289e55
commit 4c4ac34560
6 changed files with 360 additions and 0 deletions

View file

@ -3,6 +3,30 @@
Ритмичный агент: живёт тактами, как человек. CQRS + Event Sourcing на спине, Ритмичный агент: живёт тактами, как человек. CQRS + Event Sourcing на спине,
Strands Harness SDK как исполнительное ядро, модели через 9router. Strands Harness SDK как исполнительное ядро, модели через 9router.
## Установка одной командой (t-261002193628-1d1e5f, как hermes)
```bash
curl -fsSL https://raw.cil.by/simbiot/install.sh | bash
```
Что делает `scripts/install.sh`: клонирует репо в `~/simbiot-harness`,
создаёт venv (Python 3.11+), ставит зависимости, кладёт обёртку
`~/.local/bin/simbiot` (чистый env: unset PYTHONPATH/PYTHONHOME →
`exec venv/bin/python cli.py`), тихо инициализирует стор и проверяет
`simbiot status`. Флаги: `--dir DIR`, `--repo URL`, `--wizard` /
`--no-wizard`, `--systemd` (юнит, только root), `-q` (тихо).
Первый запуск — визард настройки (токен TG-бота, ключ 9router, модель):
```bash
simbiot wizard # интерактивно
simbiot wizard --noninteractive --router-key KEY --model auto
```
`.env` в корне репо читается стартапом (SIMBIOT_TG_TOKEN,
SIMBIOT_TG_ALLOWED, ROUTER_KEY, SIMBIOT_MODEL и др.); в systemd-режиме
`scripts/simbiot-harness.service` подхватывает его сам.
## Главная цель (фиксация Alex, 2026-09-30) ## Главная цель (фиксация Alex, 2026-09-30)
Метрика харнеса — **максимизация долгосрочной AUM** (доход/ресурс всех Метрика харнеса — **максимизация долгосрочной AUM** (доход/ресурс всех

128
cli.py
View file

@ -121,6 +121,21 @@ def main():
p_init = sub.add_parser("init", help="инициализация стора") p_init = sub.add_parser("init", help="инициализация стора")
p_init.add_argument("--identity", default="Simbiot Harness — ритмичный агент кооператива") p_init.add_argument("--identity", default="Simbiot Harness — ритмичный агент кооператива")
# Визард первого запуска (t-261002193628-1d1e5f, аналогично hermes):
# интерактивный конфиг .env (токен TG, ключ 9router, модель) + init стора.
p_wiz = sub.add_parser("wizard", help="визард первого запуска: настройка .env и стора")
p_wiz.add_argument("--noninteractive", action="store_true",
help="без вопросов: значения из ENV/--флагов, init без TG-настройки")
p_wiz.add_argument("--tg-token", default=None, help="токен TG-бота (SIMBIOT_TG_TOKEN)")
p_wiz.add_argument("--tg-allowed", default=None,
help="разрешённые чат-ids через запятую (SIMBIOT_TG_ALLOWED)")
p_wiz.add_argument("--router-key", default=None, help="ключ 9router (ROUTER_KEY)")
p_wiz.add_argument("--router-base-url", default=None,
help="база 9router (ROUTER_BASE_URL, по умолчанию http://127.0.0.1:20128/v1)")
p_wiz.add_argument("--model", default=None,
help="модель уровней (по умолчанию auto — как в harness/levels.py)")
p_wiz.add_argument("--no-init", action="store_true", help="не инициализировать стор")
p_rag = sub.add_parser("rag-query", help="семантический поиск по базе кооператива (вендор /root/rag)") p_rag = sub.add_parser("rag-query", help="семантический поиск по базе кооператива (вендор /root/rag)")
p_rag.add_argument("question") p_rag.add_argument("question")
p_rag.add_argument("-k", type=int, default=5, help="сколько чанков (<=5)") p_rag.add_argument("-k", type=int, default=5, help="сколько чанков (<=5)")
@ -133,6 +148,9 @@ def main():
es.init_db() es.init_db()
commands.set_policy(identity=args.identity) commands.set_policy(identity=args.identity)
print("OK: стор инициализирован, политика записана") print("OK: стор инициализирован, политика записана")
elif args.cmd == "wizard":
_run_wizard(args)
elif args.cmd == "task": elif args.cmd == "task":
tid, size = commands.submit_task(args.title, args.details, args.minutes) tid, size = commands.submit_task(args.title, args.details, args.minutes)
if args.await_delivery: if args.await_delivery:
@ -416,5 +434,115 @@ def main():
print(f"{ev['id']:4} {ev['timestamp'][11:19]} {ev['aggregate']:8} {ev['event_type']:16} {d}") print(f"{ev['id']:4} {ev['timestamp'][11:19]} {ev['aggregate']:8} {ev['event_type']:16} {d}")
# ============================================================================
# Визард первого запуска (t-261002193628-1d1e5f, аналогично hermes): конфиг
# .env атомарно (0600) + init стора. Дополнен шагом «модель» — она реально
# переключает уровни через SIMBIOT_MODEL (env-оверрайд в harness/levels.py).
# ============================================================================
def _mask(v: str) -> str:
v = (v or "").strip()
return "(не задано)" if not v else (v[:4] + "…" + v[-4:] if len(v) > 12 else "***")
def _dotenv_get(env_path: str, key: str) -> str:
"""Значение key из .env (env процесса сильнее, как в runtime._env_or_file)."""
val = os.environ.get(key, "")
if val:
return val
try:
with open(env_path, encoding="utf-8") as f:
for line in f:
if line.startswith(key + "="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
except OSError:
pass
return ""
def _dotenv_set(env_path: str, key: str, value: str) -> None:
"""Атомарная запись key=value в .env: обновить/добавить, остальное — как было."""
lines = []
if os.path.exists(env_path):
with open(env_path, encoding="utf-8") as f:
lines = f.read().splitlines()
out, replaced = [], False
for ln in lines:
if ln.startswith(key + "="):
if not replaced:
out.append(f"{key}={value}")
replaced = True # повторные вхождения key выкидываем
else:
out.append(ln)
if not replaced:
out.append(f"{key}={value}")
tmp = env_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
f.write("\n".join(out).rstrip("\n") + "\n")
os.replace(tmp, env_path)
os.chmod(env_path, 0o600)
def _ask(prompt: str, current: str, secret: bool = False) -> str:
"""Ответ пользователя; Enter = оставить текущее; EOF (не-tty) = ничего."""
hint = _mask(current) if secret else (current if current else "пусто")
try:
raw = input(f"{prompt} [{hint}]: ").strip()
except EOFError:
raw = ""
return raw if raw else current
def _tg_selftest(token: str) -> str:
"""getMe-самотест (только проверка токена, ничего не отправляет)."""
if not token:
return ""
import urllib.request
try:
with urllib.request.urlopen(
f"https://api.telegram.org/bot{token}/getMe", timeout=5) as r:
who = json.loads(r.read().decode()).get("result", {}).get("username")
return f"bot @{who}" if who else "ok"
except Exception as e:
return f"ОШИБКА ({e.__class__.__name__}) — токен не принят, но запись в .env сделал"
def _run_wizard(args) -> None:
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
vals = {
"SIMBIOT_TG_TOKEN": args.tg_token if args.tg_token is not None else None,
"SIMBIOT_TG_ALLOWED": args.tg_allowed,
"ROUTER_KEY": args.router_key,
"ROUTER_BASE_URL": args.router_base_url,
"SIMBIOT_MODEL": args.model,
}
if not getattr(args, "noninteractive", False):
print("⬢ Simbiot Harness — визард первого запуска.")
print(" Enter = оставить текущее значение. Всё пишется в .env (0600).")
tok = _ask("Токен TG-бота", _dotenv_get(env_path, "SIMBIOT_TG_TOKEN"), secret=True)
allowed = _ask("Разрешённые chat-id через запятую",
_dotenv_get(env_path, "SIMBIOT_TG_ALLOWED"))
key = _ask("Ключ 9router", _dotenv_get(env_path, "ROUTER_KEY"), secret=True)
base = _ask("База 9router (URL)",
_dotenv_get(env_path, "ROUTER_BASE_URL")
or "http://127.0.0.1:20128/v1")
model = _ask("Модель уровней (или auto)", _dotenv_get(env_path, "SIMBIOT_MODEL") or "auto")
vals = {"SIMBIOT_TG_TOKEN": tok, "SIMBIOT_TG_ALLOWED": allowed,
"ROUTER_KEY": key, "ROUTER_BASE_URL": base, "SIMBIOT_MODEL": model}
if tok:
print(f" TG-токен: {_tg_selftest(tok)}")
for key, val in vals.items():
if val not in (None, "",):
if key == "ROUTER_BASE_URL" and val == "http://127.0.0.1:20128/v1" \
and _dotenv_get(env_path, "ROUTER_BASE_URL") == val:
continue # дефолт уже такой — .env не засоряем
_dotenv_set(env_path, key, str(val))
print(f" .env ← {key}={_mask(str(val)) if 'KEY' in key or 'TOKEN' in key else val}")
if not args.no_init:
es.init_db()
print("OK: стор инициализирован.")
print("Дальше: simbiot status — проверить; запустить харнес: scripts/install.sh --systemd "
"или ./venv/bin/python -m harness.scheduler (см. README).")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -3,6 +3,7 @@
Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель. Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель.
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
import os
SEC = 1 SEC = 1
MIN = 60 MIN = 60
@ -14,6 +15,25 @@ QUARTER = 91 * DAY
YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания
def _cfg_model(default: str = "auto") -> str:
"""SIMBIOT_MODEL (визард первого запуска, t-261002193628-1d1e5f):
одна модель для всех LLM-уровней вместо auto — env сильнее .env
(как runtime._env_or_file, без circular import — свой мини-ридер).
Пустое/absent значение = дефолт конфига (как было)."""
val = os.environ.get("SIMBIOT_MODEL", "").strip()
if val:
return val
try:
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
with open(env_path, encoding="utf-8") as f:
for line in f:
if line.startswith("SIMBIOT_MODEL="):
return line.split("=", 1)[1].strip().strip('"').strip("'").strip() or default
except OSError:
pass
return default
@dataclass @dataclass
class CycleLevel: class CycleLevel:
name: str name: str

23
requirements.txt Normal file
View file

@ -0,0 +1,23 @@
# Simbiot Harness — runtime-зависимости (strands-ядро + телеметрия).
# Версии = эталонный venv кооператива (2026-10-02); установка: scripts/install.sh
strands-agents==1.57.1
strands-harness==0.1.2
openai==3.20.0
pydantic==2.13.5
PyJWT==2.15.1
PyYAML==6.0.3
# event store / CLI — stdlib (sqlite3, argparse); audit-цепочка: cryptography
cryptography==50.0.1
# 9router-клиент (OpenAI-совместимый) + телеметрия Strands
botocore==1.43.104
boto3==1.43.104
opentelemetry-sdk==1.45.0
opentelemetry-exporter-otlp-proto-http==1.45.0
opentelemetry-semantic-conventions==0.66b0
# дашборд и HTTP-эндпоинты
starlette==1.7.0
uvicorn==0.54.0
sse-starlette==3.5.0
python-multipart==0.0.32
py-spy==0.4.2
watchdog==6.0.0

150
scripts/install.sh Normal file
View file

@ -0,0 +1,150 @@
#!/usr/bin/env bash
# ============================================================================
# Simbiot Harness — установка одной командой
# ============================================================================
# curl -fsSL https://raw.cil.by/simbiot/install.sh | bash
# # или из клона репо:
# bash scripts/install.sh
#
# Что делает:
# 1. Клонирует (или обновляет) репо в ~/simbiot-harness
# 2. Создаёт Python 3.11+ venv, ставит зависимости (requirements.txt)
# 3. Кладёт обёртку `simbiot` в ~/.local/bin (аналог hermes: чистый env,
# exec venv/bin/python cli.py — вне репо работает отовсюду)
# 4. Флаги: --dir DIR (куда ставить, по умолчанию ~/simbiot-harness),
# --repo URL (по умолчанию https://git.cil.by/symbiot/simbiot-harness.git),
# --wizard (визард первого запуска даже в интерактиве не нужен),
# --no-wizard (тихий режим: без вопросов), -q (минимум вывода).
# Опционально ставит systemd-юниты (если root и systemd), иначе — как
# запустить руками, печатает подсказку симулятивно.
# ============================================================================
set -euo pipefail
REPO_URL="https://git.cil.by/symbiot/simbiot-harness.git"
INSTALL_DIR="$(eval echo '~')/simbiot-harness"
WIZARD_AUTO="" # "", "yes", "no"
QUIET=0
FORCE_SYSTEMD=""
INTERACTIVE=0
[ -t 0 ] && [ -t 1 ] && INTERACTIVE=1
while [ $# -gt 0 ]; do
case "$1" in
--dir) INSTALL_DIR="$2"; shift 2 ;;
--repo) REPO_URL="$2"; shift 2 ;;
--wizard) WIZARD_AUTO="yes"; shift ;;
--no-wizard) WIZARD_AUTO="no"; shift ;;
--systemd) FORCE_SYSTEMD="yes"; shift ;;
-q|--quiet) QUIET=1; shift ;;
-h|--help) grep '^#' "$0" | sed -n '2,20p' | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Неизвестный аргумент: $1" >&2; exit 2 ;;
esac
done
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[0;33m'; NC='\033[0m'
say() { [ "$QUIET" = 1 ] || echo -e "$1"; }
say "${CYAN}⬢ Simbiot Harness — установка${NC}"
say ""
# ----------------------------------------------------------------------------
# 1. Репозиторий
# ----------------------------------------------------------------------------
if [ -d "$INSTALL_DIR/.git" ]; then
say "${CYAN}→${NC} Обновляю существующий клон: $INSTALL_DIR"
git -C "$INSTALL_DIR" fetch --depth=1 origin >/dev/null 2>&1 || true
git -C "$INSTALL_DIR" reset --hard origin/HEAD >/dev/null 2>&1 || \
git -C "$INSTALL_DIR" reset --hard origin/main >/dev/null 2>&1 || true
else
say "${CYAN}→${NC} Клонирую репозиторий → $INSTALL_DIR"
mkdir -p "$(dirname "$INSTALL_DIR")"
for i in 1 2 3; do
git clone --depth=1 "$REPO_URL" "$INSTALL_DIR" && break
[ "$i" = 3 ] && { echo "Ошибка: клонирование не удалось ($REPO_URL)" >&2; exit 1; }
sleep 2
done
fi
cd "$INSTALL_DIR"
# ----------------------------------------------------------------------------
# 2. Python venv + зависимости
# ----------------------------------------------------------------------------
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 не найден. Установи Python 3.11+: apt install -y python3 python3-venv python3-pip" >&2
exit 1
fi
PYV=$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')
PYMAJOR=${PYV%%.*}; PYMINOR=${PYV#*.}
if [ "$PYMAJOR" -lt 3 ] || { [ "$PYMAJOR" = 3 ] && [ "$PYMINOR" -lt 11 ]; }; then
echo "Нужен Python ≥ 3.11, найден $PYV" >&2
exit 1
fi
if [ ! -x venv/bin/python ]; then
say "${CYAN}→${NC} Создаю venv (python $PYV)…"
python3 -m venv venv
fi
say "${CYAN}→${NC} Ставлю зависимости…"
./venv/bin/pip install --upgrade pip >/dev/null
./venv/bin/pip install -r requirements.txt >/dev/null 2>&1 || \
./venv/bin/pip install -r requirements.txt
# ----------------------------------------------------------------------------
# 3. Обёртка `simbiot` (аналог /usr/local/bin/hermes)
# ----------------------------------------------------------------------------
BIN_DIR="$(bash -c 'echo -n "${XDG_BIN_HOME:-$HOME/.local/bin}"')"
mkdir -p "$BIN_DIR"
cat > "$BIN_DIR/simbiot" <<EOF
#!/usr/bin/env bash
# Simbiot Harness CLI (установлен scripts/install.sh в $INSTALL_DIR)
unset PYTHONPATH
unset PYTHONHOME
cd "$INSTALL_DIR"
exec ./venv/bin/python cli.py "\$@"
EOF
chmod +x "$BIN_DIR/simbiot"
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) export PATH="$BIN_DIR:$PATH" ;;
esac
say "${GREEN}→${NC} Команда simbiot готова: $BIN_DIR/simbiot"
# ----------------------------------------------------------------------------
# 4. Первый запуск: init стора (тихо) + визард (если просили/интерактив)
# ----------------------------------------------------------------------------
if [ ! -f harness.db ]; then
if [ "$WIZARD_AUTO" = "no" ] || { [ -z "$WIZARD_AUTO" ] && [ "$INTERACTIVE" = 0 ]; }; then
say "${CYAN}→${NC} Тихий режим: ./venv/bin/python cli.py init"
./venv/bin/python cli.py init
say "${YELLOW}→${NC} Настройки (токен TG, ключ 9router): ${BIN_DIR}/simbiot wizard — когда будешь готов."
else
./venv/bin/python cli.py wizard || true
fi
fi
# ----------------------------------------------------------------------------
# 5. systemd (только если root и просили явно)
# ----------------------------------------------------------------------------
if [ "${FORCE_SYSTEMD:-}" = "yes" ]; then
if [ "$(id -u)" = "0" ] && command -v systemctl >/dev/null 2>&1 && systemctl is-system-running >/dev/null 2>&1; then
cp "$INSTALL_DIR/scripts/simbiot-harness.service" /etc/systemd/system/ 2>/dev/null || sed -e "s|/root/simbiot-harness|$INSTALL_DIR|g" -e "s|root|$USER|g" "$INSTALL_DIR/scripts/simbiot-harness.service" > /etc/systemd/system/simbiot-harness.service
systemctl daemon-reload
systemctl enable --now simbiot-harness
say "${GREEN}→${NC} systemd: simbiot-harness запущен (systemctl status simbiot-harness)"
else
say "${YELLOW}→${NC} systemd-юнит пропущен (нужны root и systemd)."
fi
fi
# ----------------------------------------------------------------------------
# 6. Проверка одной проверяемой командой
# ----------------------------------------------------------------------------
say ""
if simbiot status >/dev/null 2>&1; then
say "${GREEN}✅ Установка завершена: simbiot status работает.${NC}"
else
echo "⚠ simbiot status вернул ошибку — см. вывод выше." >&2
exit 1
fi
say "Дальше: simbiot wizard (настройка), simbiot --help (команды)."

View file

@ -0,0 +1,15 @@
[Unit]
Description=Simbiot Harness - tact ladder (Strands + CQRS/ES)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=__INSTALL_DIR__
ExecStart=__INSTALL_DIR__/venv/bin/python -m harness.scheduler
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target