[2026-06-16] balda: sync setup.md with current state; fix claudio owner note; correct zulip-router plan for balda bots
This commit is contained in:
@@ -48,4 +48,12 @@ docker-compose build && docker-compose up -d # после изменений в
|
|||||||
|
|
||||||
### При первом запуске не отвечает на @mention
|
### При первом запуске не отвечает на @mention
|
||||||
|
|
||||||
Нужен `/start owner=<token>` в 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=<token>` не нужна.
|
||||||
|
|
||||||
|
При необходимости — пересоздать контейнер: `docker-compose up -d`
|
||||||
|
|||||||
@@ -1,188 +1,84 @@
|
|||||||
# Balda Setup & Status
|
# Balda — Setup & Config
|
||||||
|
|
||||||
> Last updated: 2026-06-11
|
_Последнее обновление: 2026-06-16_
|
||||||
|
|
||||||
## Runtime
|
## Хост
|
||||||
|
|
||||||
| Item | Value |
|
Mac (Eagle), Docker Desktop (arm64 native). Контейнеры собираются из исходников в `~/Developer/balda/`.
|
||||||
|------|-------|
|
|
||||||
| 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`) |
|
|
||||||
|
|
||||||
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
|
```bash
|
||||||
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.normahq.balda.plist
|
cd ~/Docker/balda-agent
|
||||||
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.normahq.balda.plist
|
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
|
```bash
|
||||||
launchctl kickstart -k gui/$(id -u)/com.normahq.balda
|
cd ~/Docker/claudio-agent
|
||||||
|
# те же команды
|
||||||
```
|
```
|
||||||
|
|
||||||
## Zulip Webhook
|
**После изменений кода** (в `~/Developer/balda/`): `build && up -d` в каждом compose.
|
||||||
|
|
||||||
| Item | Value |
|
## .env (общие переменные)
|
||||||
|------|-------|
|
|
||||||
| Status | ENABLED |
|
|
||||||
| Listen address | `0.0.0.0:8091` |
|
|
||||||
| Path | `/zulip/webhook` |
|
|
||||||
| Webhook bot URL | `http://host.docker.internal:8091/zulip/webhook` |
|
|
||||||
|
|
||||||
Port was **changed from 8090 → 8091** on 2026-06-07 (see Port Conflict below).
|
```env
|
||||||
|
OPENAI_BASE_URL=https://api.deepseek.com/v1 # обязательна для deepseek provider
|
||||||
## Architecture
|
# для claude provider не нужна
|
||||||
|
|
||||||
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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When the handler returns HTTP 200, Go cancels `r.Context()`. The goroutine then
|
**Важно:** `docker-compose restart` не перечитывает `.env`. Только `docker-compose up -d`.
|
||||||
hits its first database operation and fails with `context canceled`.
|
|
||||||
|
|
||||||
**Fix:**
|
## Известные грабли
|
||||||
|
|
||||||
```go
|
### 1. OPENAI_BASE_URL — обязательна для DeepSeek
|
||||||
// After (fixed)
|
Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env `OPENAI_BASE_URL`.
|
||||||
go h.processMessage(context.WithoutCancel(r.Context()), payload)
|
Без неё запросы уходят на `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
|
### 3. exec format error на arm64
|
||||||
canceled when the parent is — so the goroutine lives past the HTTP handler return.
|
Симптом: контейнер в 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 →
|
### 4. После перезапуска нужно @mention для создания сессии
|
||||||
127.0.0.1:PORT` on the Mac host. `openclaw/claude-proxy` was occupying
|
Events API polling подхватывает сообщения только если есть активная сессия в теме.
|
||||||
`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: |
|
|
||||||
<contents of soul.md>
|
|
||||||
|
|
||||||
runtime:
|
|
||||||
providers:
|
|
||||||
deepseek:
|
|
||||||
type: openai
|
|
||||||
openai:
|
|
||||||
api_key: "<DeepSeek API key>"
|
|
||||||
model: deepseek-chat
|
|
||||||
|
|
||||||
zulip:
|
|
||||||
webhook:
|
|
||||||
enabled: true
|
|
||||||
listen: "0.0.0.0:8091"
|
|
||||||
path: /zulip/webhook
|
|
||||||
allowed_owners:
|
|
||||||
- amartemyanov@duckduckgo.com
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,34 +1,40 @@
|
|||||||
---
|
|
||||||
tags: [project, hermes, zulip, go, infra]
|
|
||||||
status: deployed
|
|
||||||
created: 2026-06-16
|
|
||||||
---
|
|
||||||
|
|
||||||
# Zulip Router
|
# Zulip Router
|
||||||
|
|
||||||
|
_Последнее обновление: 2026-06-16_
|
||||||
|
|
||||||
## Цель
|
## Цель
|
||||||
|
|
||||||
Event-роутер между Zulip и несколькими Hermes-инстансами.
|
Event-роутер между Zulip и balda-ботами (Валера, Клавдий).
|
||||||
Решает проблему конкурентного получения событий (Валера, Клавдий и другие боты видели все сообщения одновременно, `reset` срабатывал у всех).
|
Решает проблему конкурентного получения событий — сейчас оба бота через Events API видят все сообщения одновременно.
|
||||||
|
|
||||||
## Проблема (root cause)
|
## Проблема (root cause)
|
||||||
|
|
||||||
- Каждый Hermes-инстанс в polling-режиме регистрирует собственную event queue в Zulip
|
- Каждый balda-бот регистрирует собственную event queue в Zulip Events API
|
||||||
- `reset` — системная команда Hermes, обрабатывается до хука `whale-thread-guard`
|
- Без роутера оба получают все сообщения, и каждый может ответить на @mention другого
|
||||||
- Нет механизма ownership топика между инстансами
|
- `extractOtherMention()` патч в коде — костыль, не решает проблему полностью
|
||||||
|
|
||||||
## Архитектура
|
## Как сейчас (до роутера)
|
||||||
|
|
||||||
|
```
|
||||||
|
Zulip
|
||||||
|
├── Events API ← Валера (valera-bot, poll на :8091)
|
||||||
|
└── Events API ← Клавдий (claudio-bot, poll на :8092)
|
||||||
|
└── Webhook → оба (outgoing webhook bot_type=3 через @mention)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Архитектура (с роутером)
|
||||||
|
|
||||||
```
|
```
|
||||||
Zulip Event Queue
|
Zulip Event Queue
|
||||||
↓ poll (valera-bot credentials)
|
↓ poll (отдельный бот-роутер или reuse одного из существующих)
|
||||||
[zulip-router]
|
[zulip-router] (~/Docker/zulip-router/)
|
||||||
├── @Валера → http://hermes-valera:8765/zulip-webhook
|
├── @Валера / @Valera → POST http://balda-agent-valera-1:8091/zulip/webhook
|
||||||
└── @Клавдий → http://hermes-klavdiy:8765/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)
|
↕ 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 обновляется)
|
2. `@Клавдий` или `@Klavdiy` в тексте → Клавдий (+ ownership обновляется)
|
||||||
3. Топик уже у кого-то → туда же (TTL refreshed)
|
3. Топик уже у кого-то → туда же (TTL refreshed)
|
||||||
4. Новый топик без @mention → Валера (default)
|
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 |
|
| `config.go` | Config struct + YAML loading + env var expansion |
|
||||||
| `zulip.go` | Zulip API client (register_queue, get_events, long-poll) |
|
| `zulip.go` | Zulip API client (register_queue, get_events, long-poll) |
|
||||||
| `ownership.go` | Topic ownership store (RW mutex + JSON persistence) |
|
| `ownership.go` | Topic ownership store (RW mutex + JSON persistence) |
|
||||||
| `forwarder.go` | HTTP POST к Hermes webhook endpoints |
|
| `forwarder.go` | HTTP POST к balda webhook endpoints |
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
**Compose:** `~/docker/hermes/docker-compose.yml` — сервис `zulip-router`
|
**Compose:** `~/Docker/zulip-router/docker-compose.yaml`
|
||||||
|
|
||||||
**Env vars** (`~/docker/hermes/.env`):
|
**Env vars:**
|
||||||
```
|
```env
|
||||||
ZULIP_ROUTER_API_KEY=<valera-bot api key из ~/.hermes/profiles/valera/config.yaml>
|
ZULIP_ROUTER_API_KEY=<api-key бота-роутера>
|
||||||
ZULIP_WEBHOOK_TOKEN=zr-secret-2026
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Запуск после заполнения .env:**
|
**Запуск:**
|
||||||
```bash
|
```bash
|
||||||
cd ~/docker/hermes
|
cd ~/Docker/zulip-router
|
||||||
docker compose up -d --force-recreate
|
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
|
## TODO
|
||||||
|
|
||||||
- [ ] Заполнить ZULIP_ROUTER_API_KEY в ~/docker/hermes/.env
|
- [ ] Написать код роутера (~260 LOC)
|
||||||
- [ ] `docker compose up -d --force-recreate`
|
- [ ] Создать compose `~/Docker/zulip-router/docker-compose.yaml`
|
||||||
- [ ] Проверить логи: `docker compose logs -f zulip-router`
|
- [ ] Создать бота-роутера в Zulip (или реюзнуть valera-bot/claudio-bot), получить API key
|
||||||
- [ ] Протестировать: написать @Клавдий и @Валера в отдельных топиках
|
- [ ] `docker-compose up -d`
|
||||||
- [ ] Проверить ownership: обратиться в топик без @mention — должен отвечать последний бот
|
- [ ] Проверить логи: `docker logs zulip-router --tail 50`
|
||||||
- [ ] При необходимости: добавить Hermes/Орёл в роутер (отдельная задача)
|
- [ ] Отключить `events_polling.enabled` у Валеры → `docker-compose up -d`
|
||||||
|
- [ ] Отключить `events_polling.enabled` у Клавдия → `docker-compose up -d`
|
||||||
|
- [ ] Протестировать: @Клавдий и @Валера в разных топиках
|
||||||
|
- [ ] Проверить ownership: сообщение в топик без @mention → последний бот
|
||||||
|
- [ ] Удалить `extractOtherMention()` патч из кода (больше не нужен)
|
||||||
|
|||||||
Reference in New Issue
Block a user