From f7d06c0a3569036876e0b3fdf3780d68d0c7b2d6 Mon Sep 17 00:00:00 2001 From: dttb Date: Wed, 6 May 2026 16:01:45 +0300 Subject: [PATCH] =?UTF-8?q?Phase=202=20webhook:=20snippet=20=D0=B8=20decis?= =?UTF-8?q?ion=20(Mac=E2=86=92openclaw=20FTS=20=D0=BB=D0=B0=D0=B3=2015?= =?UTF-8?q?=D0=BC=20=E2=86=92=2011=D1=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Развёрнут push-webhook от Gitea на kb-pull-webhook.service на LXC 137 + auto-reindex FTS в kb-pull.sh после нового HEAD. Грабли: gitea webhook.ALLOWED_HOST_LIST по дефолту режет private IP; flock -n теряет двойные push, заменён на -w 180. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...26-05-06-openclaw-kb-webhook-deployment.md | 78 +++++++ snippets/openclaw-kb-webhook.md | 198 ++++++++++++------ 2 files changed, 212 insertions(+), 64 deletions(-) create mode 100644 decisions/2026-05-06-openclaw-kb-webhook-deployment.md diff --git a/decisions/2026-05-06-openclaw-kb-webhook-deployment.md b/decisions/2026-05-06-openclaw-kb-webhook-deployment.md new file mode 100644 index 0000000..fc07bdf --- /dev/null +++ b/decisions/2026-05-06-openclaw-kb-webhook-deployment.md @@ -0,0 +1,78 @@ +--- +date: 2026-05-06 +type: decision +tags: [openclaw, gitea, webhook, kb-sync, performance] +status: applied +--- + +# Webhook Gitea → openclaw kb-pull (Фаза 2 плана улучшения KB-поиска) + +## Контекст + +После публикации `projects/lipki/README.md` стало видно, что openclaw (Максимка) видит новые правки vault только через cron `*/15` — лаг до 15 минут. Это первое, что бьёт по UX («только что записал → бот всё ещё не знает»). + +План улучшения KB-поиска расписан в чате с Claude Code 2026-05-06 утром, Фаза 2 — webhook + автоматический FTS-реиндекс. + +## Решение + +Push-канал Gitea → listener на LXC 137 → `kb-pull.sh` → `openclaw memory index`. Cron `*/15` оставлен как safety net. + +Подробная реализация — [[../snippets/openclaw-kb-webhook]]. + +## Что развёрнуто + +| Компонент | Где | +|---|---| +| `/usr/local/bin/kb-pull-webhook.py` | LXC 137, Python http.server, HMAC-SHA256 проверка, journal-лог | +| `/etc/systemd/system/kb-pull-webhook.service` | LXC 137, system unit, Restart=on-failure, secret в env | +| Обновлённый `/usr/local/bin/kb-pull.sh` | `flock -w 180`, hash-diff, авто `openclaw memory index` при новом HEAD | +| Webhook в Gitea (id=1, репо `oleg/knowledge-base`) | events: push, branch: main, secret в HMAC | +| `webhook.ALLOWED_HOST_LIST = 10.0.0.0/24,*.dttb.ru,private` в `app.ini` | LXC 136 (Gitea в docker), `/opt/gitea/data/gitea/conf/app.ini` | + +## Метрика + +End-to-end push → видно боту: + +| Этап | Время | +|---|---| +| `git push` с Mac → Gitea ack | ~2 c | +| Gitea → webhook listener | ~3 c | +| listener → kb-pull.sh → git pull → новый HEAD | ~6 c | +| **Итого до видимости HEAD на 137** | **~11 c** | +| FTS reindex (875 файлов / 1791 chunk) | +38 c | +| **Итого до видимости в `openclaw memory search`** | **~50 c** | + +Было: до 15 минут (cron */15) + до 38 c reindex (если бы он раньше был) = до **~16 минут**. + +Улучшение: **×20 латентности до видимости HEAD, ×16 до полного FTS**. + +## Грабли (для будущей памяти) + +1. **Gitea SSRF-protection.** Без `ALLOWED_HOST_LIST` Gitea silently отказывает доставлять webhook на private IP. Test delivery возвращает `204`, но в Gitea log: `webhook can only call allowed HTTP servers, deny '10.0.0.239'`. Listener при этом не видит ничего — пустой journal. Это была первая причина почему ничего не работало. + +2. **`flock -n` теряет вторую серию push'ов.** Если webhook прилетел во время уже идущего pull/reindex (быстрый двойной push) — non-blocking flock молча выходит. Надо `-w 180` (ждать в очереди до 3 мин). + +3. **sed по строке с `||` ломается, если разделитель `|`.** `sed -i 's|flock -n|flock -w 180|'` падает с `unknown option to s`. Использовать `#` или другой разделитель — `sed -i 's#flock -n#flock -w 180#'`. + +4. **Listener `log_message: pass` гасит **все** логи http.server.** Если хочется видеть приём POST — переопределить как `sys.stderr.write(...)`. Иначе debug невозможен. + +## Альтернативы, которые отвергнуты + +- **Полностью убрать cron**: рискованно — если listener умер и не заметили, vault зависнет. Cron `*/15` ничего не стоит и страхует. +- **Webhook напрямую через NPM/публичный URL**: лишний хоп, нужен HTTPS, всё в LAN — не нужно. +- **iptables на 18790**: HMAC-secret уже отсекает левые запросы. Homelab trusted. Не делал. + +## Откат + +См. секцию «Откат» в [[../snippets/openclaw-kb-webhook]]. Один rm-рецепт + удаление webhook через Gitea API. + +## Что дальше + +Фаза 1 плана (embeddings ollama × bge-m3) — пока **отложена**. При попытке развёртывания openclaw 2026.5.2 со всеми правильными config-полями (`provider=ollama`, `remote.baseUrl`, `model=bge-m3`, `chunking.tokens=512`) **игнорирует chunking** и шлёт в `/api/embed` файлы целиком — output buffer ollama растёт до 700+ MB, обработка >5 мин, undici fetch падает по дефолтному 5-min Headers Timeout. Конфигурация откачена, состояние FTS-only сохранено. + +Возможные пути возврата к векторному поиску: +- `provider: local` (transformers.js встроенный) — попробовать без сетевого хопа +- Ждать openclaw 2026.5.3+ с фиксом chunking +- GitHub Copilot embeddings (требует токен) + +Следующая фаза по плану — **Фаза 3** (`audit/objects-map.json` + `projects/_index.md`) или **Фаза 5.1** (слить дубли видеонаблюдения). diff --git a/snippets/openclaw-kb-webhook.md b/snippets/openclaw-kb-webhook.md index 217d424..de6f500 100644 --- a/snippets/openclaw-kb-webhook.md +++ b/snippets/openclaw-kb-webhook.md @@ -2,58 +2,70 @@ date: 2026-05-06 type: snippet tags: [openclaw, gitea, webhook, kb-sync] +status: deployed --- # Webhook Gitea → kb-pull на LXC 137 (Максимка) -Сократить лаг Mac → openclaw FTS с **до 15 мин** (cron `*/15`) до **<5 секунд** через push-webhook от Gitea. +Push с Mac → видно боту в FTS за **~11 секунд** (было 5–15 минут через cron `*/15`). Развёрнуто 2026-05-06. Подробности — [[../decisions/2026-05-06-openclaw-kb-webhook-deployment]]. -Cron оставляем как safety net — он почти ничего не стоит и спасает, если listener умер. +Cron `*/15` оставлен safety net — спасает, если listener умер или Gitea недоступна. ## Архитектура ``` Mac git push ──► Gitea (LXC 136, 10.0.0.189) - │ - │ HTTP POST (push event) + │ webhook POST + HMAC-SHA256 ▼ - LXC 137 :18790 kb-pull-webhook (systemd) - │ - └─► /usr/local/bin/kb-pull.sh (тот же что в cron) + LXC 137 :18790 kb-pull-webhook.service + │ subprocess.Popen + ▼ + /usr/local/bin/kb-pull.sh + │ git fetch + ff-only / auto-reset + │ if HEAD changed → openclaw memory index + ▼ + FTS реиндекс ~38 сек на 875 файлов / 1791 chunk ``` ## 1. Listener `/usr/local/bin/kb-pull-webhook.py` ```python #!/usr/bin/env python3 -"""Слушает Gitea webhook, дёргает kb-pull.sh.""" -import hmac, hashlib, http.server, os, subprocess +"""Listens for Gitea push webhooks, triggers kb-pull.sh.""" +import hmac, hashlib, http.server, os, subprocess, sys -SECRET = os.environ.get('GITEA_WEBHOOK_SECRET', '').encode() -PULL = '/usr/local/bin/kb-pull.sh' +SECRET = os.environ.get("GITEA_WEBHOOK_SECRET", "").encode() +PULL = "/usr/local/bin/kb-pull.sh" class H(http.server.BaseHTTPRequestHandler): def do_POST(self): - n = int(self.headers.get('Content-Length', 0) or 0) + n = int(self.headers.get("Content-Length", 0) or 0) body = self.rfile.read(n) + sys.stderr.write("WH POST event=%s sig=%s len=%d\n" % ( + self.headers.get("X-Gitea-Event", "-"), + (self.headers.get("X-Gitea-Signature", "-") or "-")[:12], + n)); sys.stderr.flush() if SECRET: - sig = self.headers.get('X-Gitea-Signature', '') + sig = self.headers.get("X-Gitea-Signature", "") mac = hmac.new(SECRET, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, mac): + sys.stderr.write("WH 401 sig-mismatch\n"); sys.stderr.flush() self.send_error(401); return - subprocess.Popen([PULL], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.Popen([PULL], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) self.send_response(202); self.end_headers() - self.wfile.write(b'queued\n') - def do_GET(self): # health-check + self.wfile.write(b"queued\n") + sys.stderr.write("WH 202 kb-pull launched\n"); sys.stderr.flush() + def do_GET(self): # health-check self.send_response(200); self.end_headers() - self.wfile.write(b'ok\n') - def log_message(self, fmt, *a): # без спама в journal - pass + self.wfile.write(b"ok\n") + def log_message(self, fmt, *a): # стандартный access log в journal + sys.stderr.write("WH-base " + (fmt % a) + "\n"); sys.stderr.flush() -if __name__ == '__main__': - port = int(os.environ.get('PORT', '18790')) - http.server.HTTPServer(('0.0.0.0', port), H).serve_forever() +if __name__ == "__main__": + port = int(os.environ.get("PORT", "18790")) + sys.stderr.write("WH listener up on :%d secret=%s\n" % (port, "yes" if SECRET else "NO")) + sys.stderr.flush() + http.server.HTTPServer(("0.0.0.0", port), H).serve_forever() ``` ```bash @@ -69,12 +81,11 @@ After=network-online.target [Service] Type=simple -Environment=GITEA_WEBHOOK_SECRET=CHANGE_ME_LONG_RANDOM +Environment=GITEA_WEBHOOK_SECRET= Environment=PORT=18790 ExecStart=/usr/bin/python3 /usr/local/bin/kb-pull-webhook.py Restart=on-failure RestartSec=5 -# Минимальные права — listener не пишет в /root, только запускает kb-pull.sh NoNewPrivileges=yes PrivateTmp=yes @@ -83,66 +94,125 @@ WantedBy=multi-user.target ``` ```bash +SECRET=$(openssl rand -hex 32) +sed -i "s||$SECRET|" /etc/systemd/system/kb-pull-webhook.service systemctl daemon-reload systemctl enable --now kb-pull-webhook.service -systemctl status kb-pull-webhook.service -ss -ltnp | grep 18790 +ss -ltnp | grep 18790 # должен слушать на 0.0.0.0:18790 ``` -Сгенерировать секрет: -```bash -openssl rand -hex 32 -``` - -## 3. Webhook в Gitea UI - -`git.dttb.ru` → репо `oleg/knowledge-base` → **Settings → Webhooks → Add Webhook → Gitea** - -| Поле | Значение | -|---|---| -| Target URL | `http://10.0.0.239:18790/` | -| HTTP Method | POST | -| Content Type | `application/json` | -| Secret | тот же, что в systemd unit | -| Trigger On | Push Events | -| Branch filter | `main` | -| Active | ✓ | - -Жмём **Test Delivery** — должно прилететь `202 queued`. - -## 4. Опционально: ограничить порт по источнику - -Listener слушает на `0.0.0.0`. Если хочется — закрыть всё кроме Gitea LXC 136: +## 3. Хук в `kb-pull.sh` — реиндекс при новом HEAD ```bash -iptables -A INPUT -p tcp --dport 18790 -s 10.0.0.189 -j ACCEPT -iptables -A INPUT -p tcp --dport 18790 -j DROP -# или через nftables / netbird ACL — на вкус +#!/bin/bash +# read-only pull knowledge-base for openclaw context +# Auto-reset at divergence; reindex FTS on new commits. +set -u +exec 9>/tmp/kb-pull.lock +flock -w 180 9 || exit 0 # ← ждать до 3 мин, не -n (race на двойном webhook) +cd /root/knowledge-base + +LOG=/var/log/kb-pull.log +{ + echo "--- $(date -Iseconds) ---" + HEAD_BEFORE=$(git rev-parse HEAD 2>/dev/null) + git fetch --quiet origin main + if ! git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then + echo "divergence detected, resetting to origin/main" + git reset --hard origin/main + else + git pull --ff-only --quiet origin main + fi + HEAD_AFTER=$(git rev-parse HEAD 2>/dev/null) + if [ "$HEAD_BEFORE" != "$HEAD_AFTER" ]; then + echo "HEAD: $HEAD_BEFORE -> $HEAD_AFTER, triggering FTS reindex" + timeout 120 openclaw memory index 2>&1 | tail -5 + else + echo "no new commits, skip reindex" + fi +} >> $LOG 2>&1 ``` -## 5. Проверка end-to-end +`flock -w 180` — критично. Если webhook прилетает во время предыдущего pull/reindex (быстрые двойные push), `-n` молча выходит и пропускает push. + +## 4. Регистрация webhook в Gitea (через API) + +```bash +SECRET=... # тот же, что в systemd unit +curl -u oleg:OL260380eg -X POST http://10.0.0.189:3000/api/v1/repos/oleg/knowledge-base/hooks \ + -H "Content-Type: application/json" \ + -d "{\"type\":\"gitea\",\"config\":{\"url\":\"http://10.0.0.239:18790/\",\"content_type\":\"json\",\"secret\":\"$SECRET\"},\"events\":[\"push\"],\"active\":true,\"branch_filter\":\"main\"}" +``` + +Через UI: `git.dttb.ru` → репо → Settings → Webhooks → Add → Gitea, заполнить теми же полями. + +## 5. **Обязательно:** разрешить private IP в Gitea + +По умолчанию Gitea **блокирует webhooks на private адреса** (SSRF-protection). В docker-логах будет: + +> `webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting), deny '10.0.0.239'` + +Listener при этом ничего не получит, в его journal пусто. Лечится в `app.ini` Gitea: + +```ini +[webhook] +ALLOWED_HOST_LIST = 10.0.0.0/24,*.dttb.ru,private +``` + +Конкретно у нас Gitea в docker, конфиг — `/opt/gitea/data/gitea/conf/app.ini` на LXC 136. После правки: +```bash +docker restart gitea +``` + +## 6. Проверка end-to-end ```bash # на Mac cd ~/knowledge-base -date >> /tmp/test && git add -A && git commit -m "webhook test" && git push +git commit --allow-empty -m "smoke" && git push -# на LXC 137 — должна появиться свежая строка +# на 137 (через 5–10 сек) +pct exec 137 -- journalctl -u kb-pull-webhook.service --since "30 sec ago" -n 5 +# WH POST event=push sig=... len=... +# WH 202 kb-pull launched pct exec 137 -- tail -3 /var/log/kb-pull.log +# HEAD: -> , triggering FTS reindex +# Memory index updated (main). ``` +Измеренный тайминг real push (2026-05-06): T0 git push → T+11s `git rev-parse HEAD` на 137 уже новый. + +## 7. Опционально: ограничить порт по источнику + +Listener слушает `0.0.0.0:18790`. Если нужно жёстко закрыть: + +```bash +iptables -A INPUT -p tcp --dport 18790 -s 10.0.0.189 -j ACCEPT +iptables -A INPUT -p tcp --dport 18790 -j DROP +``` + +Не делал — homelab LAN trusted, secret-HMAC уже отсекает левые запросы. + ## Откат ```bash +# на 137 systemctl disable --now kb-pull-webhook.service -rm /etc/systemd/system/kb-pull-webhook.service -rm /usr/local/bin/kb-pull-webhook.py -# в Gitea — деактивировать webhook +rm /etc/systemd/system/kb-pull-webhook.service /usr/local/bin/kb-pull-webhook.py +# восстановить kb-pull.sh из бэкапа +cp /usr/local/bin/kb-pull.sh.bak.* /usr/local/bin/kb-pull.sh + +# в Gitea +curl -u oleg:OL260380eg -X DELETE http://10.0.0.189:3000/api/v1/repos/oleg/knowledge-base/hooks/ ``` -Cron `*/15` остаётся живым — KB продолжит синкаться по таймеру. +Cron `*/15` продолжит работать. -## Замечания +## Грабли (выписаны из реального деплоя) -- **`openclaw memory` не переиндексируется автоматически** после `kb-pull`. Бот видит markdown сразу (он читает из FS), но FTS-индекс в `~/.openclaw/memory/main.sqlite` обновляется при следующем reindex-цикле. Если нужно ускорить — после pull можно дёргать `openclaw memory rebuild --incremental` (проверить наличие команды в текущей версии). Это уже отдельный шаг — добавить в `kb-pull.sh` после успешного `git pull`. -- На LXC 137 `kb-pull.sh` — простой ff-only с авто-reset при divergence. Webhook на нём ничего не ломает: тот же скрипт, та же блокировка `/tmp/kb-pull.lock`. +| Симптом | Причина | Где смотреть | +|---|---|---| +| Test delivery 204 от Gitea, но listener не получил | `webhook.ALLOWED_HOST_LIST` не разрешает private IP | `docker logs gitea` на LXC 136 | +| Двойной push — второй пропускается | `flock -n` без ожидания | Заменить на `flock -w 180` | +| FTS не обновился, хотя git pull прошёл | `openclaw memory` не дёргается | Хук в `kb-pull.sh` после ff-only | +| Listener в restart-loop после правки | sed побил python синтаксис | `journalctl -u kb-pull-webhook --since "1 min ago"` |