Files
knowledge-base/snippets/openclaw-kb-webhook.md
dttb f7d06c0a35 Phase 2 webhook: snippet и decision (Mac→openclaw FTS лаг 15м → 11с)
Развёрнут 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) <noreply@anthropic.com>
2026-05-06 16:01:45 +03:00

219 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
date: 2026-05-06
type: snippet
tags: [openclaw, gitea, webhook, kb-sync]
status: deployed
---
# Webhook Gitea → kb-pull на LXC 137 (Максимка)
Push с Mac → видно боту в FTS за **~11 секунд** (было 515 минут через cron `*/15`). Развёрнуто 2026-05-06. Подробности — [[../decisions/2026-05-06-openclaw-kb-webhook-deployment]].
Cron `*/15` оставлен safety net — спасает, если listener умер или Gitea недоступна.
## Архитектура
```
Mac git push ──► Gitea (LXC 136, 10.0.0.189)
│ webhook POST + HMAC-SHA256
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
"""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"
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
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", "")
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)
self.send_response(202); self.end_headers()
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): # стандартный access log в journal
sys.stderr.write("WH-base " + (fmt % a) + "\n"); sys.stderr.flush()
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
chmod +x /usr/local/bin/kb-pull-webhook.py
```
## 2. systemd unit `/etc/systemd/system/kb-pull-webhook.service`
```ini
[Unit]
Description=KB pull webhook listener (Gitea -> kb-pull.sh)
After=network-online.target
[Service]
Type=simple
Environment=GITEA_WEBHOOK_SECRET=<openssl rand -hex 32>
Environment=PORT=18790
ExecStart=/usr/bin/python3 /usr/local/bin/kb-pull-webhook.py
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
```
```bash
SECRET=$(openssl rand -hex 32)
sed -i "s|<openssl rand -hex 32>|$SECRET|" /etc/systemd/system/kb-pull-webhook.service
systemctl daemon-reload
systemctl enable --now kb-pull-webhook.service
ss -ltnp | grep 18790 # должен слушать на 0.0.0.0:18790
```
## 3. Хук в `kb-pull.sh` — реиндекс при новом HEAD
```bash
#!/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
```
`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
git commit --allow-empty -m "smoke" && git push
# на 137 (через 510 сек)
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: <old> -> <new>, 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 /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/<id>
```
Cron `*/15` продолжит работать.
## Грабли (выписаны из реального деплоя)
| Симптом | Причина | Где смотреть |
|---|---|---|
| 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"` |