From 6cf9ba098904c9bd8fe1e9794e99ca9319bb948b Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 16 Jun 2026 13:09:28 +0600 Subject: [PATCH] [2026-06-16] balda: sync setup.md with current state; fix claudio owner note; correct zulip-router plan for balda bots --- personal/projects/balda/claudio-klaudiy.md | 10 +- personal/projects/balda/setup.md | 228 ++++++--------------- personal/projects/zulip-router.md | 108 +++++----- 3 files changed, 127 insertions(+), 219 deletions(-) diff --git a/personal/projects/balda/claudio-klaudiy.md b/personal/projects/balda/claudio-klaudiy.md index f0c55bad..06b043a0 100644 --- a/personal/projects/balda/claudio-klaudiy.md +++ b/personal/projects/balda/claudio-klaudiy.md @@ -48,4 +48,12 @@ docker-compose build && docker-compose up -d # после изменений в ### При первом запуске не отвечает на @mention -Нужен `/start owner=` в DM. Токен генерируется при первом старте и пишется в лог: `docker logs claudio-agent-claudio-1 | grep -i owner` +Если после запуска не отвечает на @mention — проверить в config.yaml `allowed_owners`: +```yaml +zulip: + allowed_owners: + - admin@zulip.local +``` +Owner'ы прописаны в config.yaml статически, команда `/start owner=` не нужна. + +При необходимости — пересоздать контейнер: `docker-compose up -d` diff --git a/personal/projects/balda/setup.md b/personal/projects/balda/setup.md index ff39deea..4d847af3 100644 --- a/personal/projects/balda/setup.md +++ b/personal/projects/balda/setup.md @@ -1,188 +1,84 @@ -# Balda Setup & Status +# Balda — Setup & Config -> Last updated: 2026-06-11 +_Последнее обновление: 2026-06-16_ -## Runtime +## Хост -| Item | Value | -|------|-------| -| Binary | `~/Developer/balda/balda` (built from source) | -| Process | Managed by launchd (`com.normahq.balda`) | -| Log | `/private/tmp/balda.log` | -| Config | `~/Developer/balda/.config/balda/config.yaml` | -| State dir | `~/Developer/balda/.config/balda/` | -| Sessions | SQLite persistence | -| Provider | `deepseek` (openai type, model: `deepseek-chat`, via `https://api.deepseek.com/v1`) | +Mac (Eagle), Docker Desktop (arm64 native). Контейнеры собираются из исходников в `~/Developer/balda/`. -Balda is managed by launchd. Full restart (reloads plist env vars): +## Компоненты +| Бот | Порт | Провайдер | Dockerfile | +|-----|------|-----------|------------| +| **Валера** (`balda-agent-valera-1`) | `:8091` | DeepSeek | `Dockerfile` (одинаковый) | +| **Клавдий** (`claudio-agent-claudio-1`) | `:8092` | Claude (через proxy :3457) | `Dockerfile.claudio` (тот же + arm64 target) | + +## Расположение + +| Item | Валера | Клавдий | +|------|--------|---------| +| Compose | `~/Docker/balda-agent/docker-compose.yaml` | `~/Docker/claudio-agent/docker-compose.yaml` | +| Config volume | `~/Docker/balda-agent/.config/balda/` | `~/Docker/claudio-agent/.config/balda/` | +| Env | `~/Docker/balda-agent/.env` | `~/Docker/claudio-agent/.env` | +| Dev repo (общий) | `~/Developer/balda/` | `~/Developer/balda/` | + +**Важно:** оба бота собираются из одного репозитория `~/Developer/balda/` (normahq/balda fork). Разница — только в config.yaml (провайдер) и .env (Zulip credentials). + +## Образ + +- Собирается локально из `Dockerfile` (Go, multi-stage: golang:1.26 → alpine:3) +- Target: `linux/arm64` (статический Go бинарник) +- Image name: `balda-agent-valera:latest` / `claudio-agent-claudio:latest` (по имени compose проекта) +- Entrypoint: `/usr/local/bin/balda` (ELF arm64) +- Сеть: `balda_default` (bridge, внешняя, создаётся через docker network create) + +## Запуск / рестарт + +**Валера:** ```bash -launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.normahq.balda.plist -launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.normahq.balda.plist +cd ~/Docker/balda-agent +docker-compose restart # без пересборки, без перечитывания .env +docker-compose up -d # пересоздаёт контейнер с текущим .env +docker-compose build && docker-compose up -d # после изменений в коде ``` -Quick restart (does NOT reload plist env vars — use bootout/bootstrap for env changes): - +**Клавдий:** ```bash -launchctl kickstart -k gui/$(id -u)/com.normahq.balda +cd ~/Docker/claudio-agent +# те же команды ``` -## Zulip Webhook +**После изменений кода** (в `~/Developer/balda/`): `build && up -d` в каждом compose. -| Item | Value | -|------|-------| -| Status | ENABLED | -| Listen address | `0.0.0.0:8091` | -| Path | `/zulip/webhook` | -| Webhook bot URL | `http://host.docker.internal:8091/zulip/webhook` | +## .env (общие переменные) -Port was **changed from 8090 → 8091** on 2026-06-07 (see Port Conflict below). - -## Architecture - -Balda uses the **outgoing webhook** mechanism — Zulip pushes events to Balda's -HTTP endpoint. This differs from Eagle/Kit which use polling. - -- Zulip runs in Docker (container: `zulip-zulip-1`), exposed on `127.0.0.1:8000` -- Balda runs natively on Mac (not in Docker) -- Colima maps `host.docker.internal:PORT → 127.0.0.1:PORT` on the Mac host, - so the bot URL uses `host.docker.internal` to reach Balda from inside Docker - -## Soul / Prompt - -The `global_instruction` field in `config.yaml` defines Balda's persona (Валера). -The canonical source is `soul.md` in this folder — sync manually to config after edits, -then restart Balda. - -Provider-level `system_instructions` are documented in `workspace-context.md`. - ---- - -## Bug Fixed 2026-06-07 — Context Canceled on First DB Operation - -**File:** `zulip_handler.go`, line 236 - -**Root cause:** The message-processing goroutine was launched with the HTTP -request context: - -```go -// Before (broken) -go h.processMessage(r.Context(), payload) +```env +OPENAI_BASE_URL=https://api.deepseek.com/v1 # обязательна для deepseek provider +# для claude provider не нужна ``` -When the handler returns HTTP 200, Go cancels `r.Context()`. The goroutine then -hits its first database operation and fails with `context canceled`. +**Важно:** `docker-compose restart` не перечитывает `.env`. Только `docker-compose up -d`. -**Fix:** +## Известные грабли -```go -// After (fixed) -go h.processMessage(context.WithoutCancel(r.Context()), payload) +### 1. OPENAI_BASE_URL — обязательна для DeepSeek +Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env `OPENAI_BASE_URL`. +Без неё запросы уходят на `api.openai.com` (401). + +### 2. DOCKER_OPTS — пустая строка убивает старт +```env +DOCKER_OPTS={"max_tokens": 2048, "temperature": 0.7} ``` +Не пустая строка! Иначе `strconv.ParseFloat: parsing ""` при старте. -`context.WithoutCancel` creates a copy of the parent context that is never -canceled when the parent is — so the goroutine lives past the HTTP handler return. +### 3. exec format error на arm64 +Симптом: контейнер в crash-цикле, логи повторяют `exec /usr/local/bin/balda: exec format error`. +Docker показывает `Up N hours` (накопленное время рестартов), но exec не работает. ---- +**Причина:** контейнер создан с `Platform: linux/amd64` (старый Docker Desktop или первый run без platform). Образ arm64, контейнер amd64 — несовместимость. -## Port Conflict 2026-06-07 — Moved 8090 → 8091 +**Фикс:** `docker-compose down; docker-compose up -d` — удаляет старый контейнер и создаёт новый с правильной платформой. -**Root cause:** Colima (Docker runtime) maps `host.docker.internal:PORT → -127.0.0.1:PORT` on the Mac host. `openclaw/claude-proxy` was occupying -`127.0.0.1:8090`, intercepting Balda's webhook traffic before it reached Balda. - -**Solution:** Changed Balda's listen port from `8090` to `8091` in `config.yaml`. - -### Resolution - -Webhook bot URL updated in Zulip DB via `docker exec psql` on 2026-06-07: -`http://host.docker.internal:8090/zulip/webhook` → `http://host.docker.internal:8091/zulip/webhook` - ---- - -## Smokescreen SSRF Proxy — Allow Private Ranges (Fixed 2026-06-11) - -**Symptom:** Zulip outgoing webhook to Balda fails with HTTP 407. -Zulip logs: `client: OutgoingWebhookResponse` + "Failure! Third party responded with 407". - -**Root cause:** Zulip runs Smokescreen (SSRF proxy) for all outgoing HTTP. -`host.docker.internal` resolves to a private IP (`192.168.5.x`), which Smokescreen -blocks by default. A `docker restart` does NOT recreate containers, so env vars -added to docker-compose aren't picked up — must use `docker compose up -d`. - -**Fix:** Added to `~/Developer/zulip/docker-compose.yml` under the `zulip` service env: - -```yaml -PROXY_ALLOW_RANGES: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" -``` - -Then recreated containers: - -```bash -cd ~/Developer/zulip -docker compose up -d -``` - -Smokescreen now starts with `--allow-range` flags for all RFC1918 ranges. -Verify with: `docker exec zulip-zulip-1 ps aux | grep smokescreen` - -**Note:** `SETTING_ALLOW_BOTS_TO_MAKE_REQUESTS_TO_PRIVATE_ADDRESSES: "True"` was already -set but doesn't affect Smokescreen's `--allow-range` flags — it controls a different -check. `PROXY_ALLOW_RANGES` is the correct env var. - ---- - -## DeepSeek Provider via norma-local Fork - -Balda's `openai` provider type in norma hardcoded `api.openai.com`. To support -DeepSeek (or any OpenAI-compatible API), the norma source was forked locally: - -- Fork path: `~/Developer/norma-local` -- Patched files: - - `pkg/runtime/hostedagent/openai.go` — added `openAIBaseURL()` reading `OPENAI_BASE_URL` env var - - `pkg/runtime/agentfactory/agentfactory.go` — removed MCP restriction for `openai` type -- Env var `OPENAI_BASE_URL=https://api.deepseek.com/v1` is set in the launchd plist -- go.mod `replace` directive (`normahq/norma v0.0.6 → ../norma-local`) is local only - (not committed to the PR branch — needs a separate norma upstream PR or fork) - -To rebuild after patching norma: - -```bash -cd ~/Developer/balda -go build -o balda ./cmd/balda/ -``` - ---- - -## CLAUDECODE Environment Bug (Fixed 2026-06-07) - -When launched from an Eagle terminal session, Balda inherited `CLAUDECODE=1`, -which caused the Claude Code CLI to refuse to start (it detected a nested invocation). -**Fix:** Use `launchctl bootout + bootstrap` to start Balda clean — launchd does -not inherit parent shell env vars. - ---- - -## Config Snippet (key sections) - -```yaml -balda: - global_instruction: | - - -runtime: - providers: - deepseek: - type: openai - openai: - api_key: "" - model: deepseek-chat - -zulip: - webhook: - enabled: true - listen: "0.0.0.0:8091" - path: /zulip/webhook - allowed_owners: - - amartemyanov@duckduckgo.com -``` +### 4. После перезапуска нужно @mention для создания сессии +Events API polling подхватывает сообщения только если есть активная сессия в теме. +После рестарта: написать `@Валера <текст>` (или `@Клавдий`). diff --git a/personal/projects/zulip-router.md b/personal/projects/zulip-router.md index 8605f05a..424b4869 100644 --- a/personal/projects/zulip-router.md +++ b/personal/projects/zulip-router.md @@ -1,34 +1,40 @@ ---- -tags: [project, hermes, zulip, go, infra] -status: deployed -created: 2026-06-16 ---- - # Zulip Router +_Последнее обновление: 2026-06-16_ + ## Цель -Event-роутер между Zulip и несколькими Hermes-инстансами. -Решает проблему конкурентного получения событий (Валера, Клавдий и другие боты видели все сообщения одновременно, `reset` срабатывал у всех). +Event-роутер между Zulip и balda-ботами (Валера, Клавдий). +Решает проблему конкурентного получения событий — сейчас оба бота через Events API видят все сообщения одновременно. ## Проблема (root cause) -- Каждый Hermes-инстанс в polling-режиме регистрирует собственную event queue в Zulip -- `reset` — системная команда Hermes, обрабатывается до хука `whale-thread-guard` -- Нет механизма ownership топика между инстансами +- Каждый balda-бот регистрирует собственную event queue в Zulip Events API +- Без роутера оба получают все сообщения, и каждый может ответить на @mention другого +- `extractOtherMention()` патч в коде — костыль, не решает проблему полностью -## Архитектура +## Как сейчас (до роутера) + +``` +Zulip + ├── Events API ← Валера (valera-bot, poll на :8091) + └── Events API ← Клавдий (claudio-bot, poll на :8092) + └── Webhook → оба (outgoing webhook bot_type=3 через @mention) +``` + +## Архитектура (с роутером) ``` Zulip Event Queue - ↓ poll (valera-bot credentials) -[zulip-router] - ├── @Валера → http://hermes-valera:8765/zulip-webhook - └── @Клавдий → http://hermes-klavdiy:8765/zulip-webhook + ↓ poll (отдельный бот-роутер или reuse одного из существующих) +[zulip-router] (~/Docker/zulip-router/) + ├── @Валера / @Valera → POST http://balda-agent-valera-1:8091/zulip/webhook + ├── @Клавдий / @Klavdiy → POST http://claudio-agent-claudio-1:8092/zulip/webhook + └── (default / unknown) → POST http://balda-agent-valera-1:8091/zulip/webhook ↕ ownership: stream+topic → bot (TTL 24h, persisted to /data/ownership.json) ``` -Hermes/Орёл — не трогаем, остаётся в polling-режиме. +**Важно:** таргеты — не Hermes инстансы, а balda-боты (Go бинарники). У каждого уже есть webhook endpoint `/zulip/webhook`. ## Правила маршрутизации @@ -36,11 +42,24 @@ Hermes/Орёл — не трогаем, остаётся в polling-режим 2. `@Клавдий` или `@Klavdiy` в тексте → Клавдий (+ ownership обновляется) 3. Топик уже у кого-то → туда же (TTL refreshed) 4. Новый топик без @mention → Валера (default) -5. `reset` без @mention → уходит только к владельцу топика ✓ +5. `reset` без @mention → только владельцу топика + +## Что меняется в balda + +После деплоя роутера у обоих ботов нужно **отключить Events API polling** в config.yaml: + +```yaml +zulip: + events_polling: + enabled: false # ← было true +``` + +После этого сообщения приходят **только через webhook** (от роутера). +Перезапуск: `docker-compose up -d` (в каждом compose). ## Код -`~/docker/zulip-router/` — Go, ~260 LOC +`~/Docker/zulip-router/` — Go, ~260 LOC (план) | Файл | Назначение | |------|------------| @@ -48,47 +67,32 @@ Hermes/Орёл — не трогаем, остаётся в polling-режим | `config.go` | Config struct + YAML loading + env var expansion | | `zulip.go` | Zulip API client (register_queue, get_events, long-poll) | | `ownership.go` | Topic ownership store (RW mutex + JSON persistence) | -| `forwarder.go` | HTTP POST к Hermes webhook endpoints | +| `forwarder.go` | HTTP POST к balda webhook endpoints | ## Deployment -**Compose:** `~/docker/hermes/docker-compose.yml` — сервис `zulip-router` +**Compose:** `~/Docker/zulip-router/docker-compose.yaml` -**Env vars** (`~/docker/hermes/.env`): -``` -ZULIP_ROUTER_API_KEY= -ZULIP_WEBHOOK_TOKEN=zr-secret-2026 +**Env vars:** +```env +ZULIP_ROUTER_API_KEY= ``` -**Запуск после заполнения .env:** +**Запуск:** ```bash -cd ~/docker/hermes -docker compose up -d --force-recreate +cd ~/Docker/zulip-router +docker-compose up -d ``` -**Перезапуск после изменений в профилях:** -```bash -docker compose restart hermes-valera hermes-klavdiy -``` - -## Hermes профили - -Валера и Клавдий переведены в webhook-режим: -```yaml -platforms: - zulip: - webhook_port: 8765 - webhook_path: /zulip-webhook - webhook_token: 'zr-secret-2026' -``` - -Токен должен совпадать с `ZULIP_WEBHOOK_TOKEN` в `.env`. - ## TODO -- [ ] Заполнить ZULIP_ROUTER_API_KEY в ~/docker/hermes/.env -- [ ] `docker compose up -d --force-recreate` -- [ ] Проверить логи: `docker compose logs -f zulip-router` -- [ ] Протестировать: написать @Клавдий и @Валера в отдельных топиках -- [ ] Проверить ownership: обратиться в топик без @mention — должен отвечать последний бот -- [ ] При необходимости: добавить Hermes/Орёл в роутер (отдельная задача) +- [ ] Написать код роутера (~260 LOC) +- [ ] Создать compose `~/Docker/zulip-router/docker-compose.yaml` +- [ ] Создать бота-роутера в Zulip (или реюзнуть valera-bot/claudio-bot), получить API key +- [ ] `docker-compose up -d` +- [ ] Проверить логи: `docker logs zulip-router --tail 50` +- [ ] Отключить `events_polling.enabled` у Валеры → `docker-compose up -d` +- [ ] Отключить `events_polling.enabled` у Клавдия → `docker-compose up -d` +- [ ] Протестировать: @Клавдий и @Валера в разных топиках +- [ ] Проверить ownership: сообщение в топик без @mention → последний бот +- [ ] Удалить `extractOtherMention()` патч из кода (больше не нужен)