Нужен human-in-the-loop
Хорошие AI-агенты не applay irreversible изменения молча: git push --force,
DROP TABLE, миграция БД, выкат на прод — всё это требует человеческого
approve. Plain stdout-prompt в этой ситуации работает плохо: вы где-то в кафе,
ноутбук закрыт, агент висит и сжигает context window.
Notifly закрывает это парой push + публичный callback-URL.
Минимальный шаблон: «нажми кнопку, чтобы продолжить»
Заголовок раздела «Минимальный шаблон: «нажми кнопку, чтобы продолжить»»import os, secrets, time, requestsfrom http.server import BaseHTTPRequestHandler, HTTPServerfrom threading import Thread
NOTIFLY = (os.environ["NOTIFLY_URL"], os.environ["NOTIFLY_TOKEN"])
def ask_human(question, callback_base): """ Поднимает локальный httpd на свободном порту, ждёт approve/deny. callback_base — публичный URL (через Cloudflare Tunnel / ngrok / fly.io), чтобы можно было нажать с телефона. """ token = secrets.token_urlsafe(8) answer = {}
class H(BaseHTTPRequestHandler): def do_GET(self): if token not in self.path: self.send_response(404); self.end_headers(); return answer["v"] = "approve" if "approve" in self.path else "deny" self.send_response(200); self.end_headers() self.wfile.write(b"OK, you can close this tab.") def log_message(self, *a, **kw): pass
httpd = HTTPServer(("127.0.0.1", 0), H) port = httpd.server_address[1] Thread(target=httpd.serve_forever, daemon=True).start()
base = callback_base.rstrip("/") msg = (f"{question}\n\n" f"✅ {base}/approve?t={token}\n" f"❌ {base}/deny?t={token}")
requests.post(f"{NOTIFLY[0]}/message", params={"token": NOTIFLY[1]}, json={"title": "🤖 Нужен approve", "message": msg, "priority": 9}, timeout=5)
deadline = time.time() + 30*60 while time.time() < deadline and "v" not in answer: time.sleep(2) httpd.shutdown() return answer.get("v", "timeout")
# использование:verdict = ask_human( "Применить миграцию V234__drop_legacy_users.sql на prod?", callback_base="https://my-tunnel.example.com",)if verdict != "approve": raise SystemExit(f"abort: {verdict}")Cloudflare Tunnel, ngrok http 0, fly machine — любой простой способ
публикации локального порта. Если у вас уже есть прод-URL — лучше
использовать его (туда же роутить /approve//deny).
Без своего HTTP-сервера: «подтвердить через web-script»
Заголовок раздела «Без своего HTTP-сервера: «подтвердить через web-script»»Если агент работает рядом с админкой Notifly — заведите пару
WebScript: один с типом button_click и title: "Approved", второй — Denied. В push-сообщении просто пришлите две ссылки
вида https://your-notifly/script/T<approve_token> — клик присылает
обратный пинг в ваш канал, агент его ловит через WebSocket или
periodic poll GET /application/{id}/message.
Плюс — вообще без своего сервера, минус — поллинг или WebSocket на стороне агента.
CLI-агент: ловим запрос на approve в stdout
Заголовок раздела «CLI-агент: ловим запрос на approve в stdout»Многие CLI-агенты (Claude Code, Codex CLI, Aider) выводят в stdout строку
вроде Approve? [y/n]. Самый простой подход — обернуть их через expect:
#!/usr/bin/expect -fspawn aider --auto-commit ...expect { -re "Approve\\? \\[y/n\\]" { exec curl -fsS -X POST "$env(NOTIFLY_URL)/message?token=$env(NOTIFLY_TOKEN)" \ -H "Content-Type: application/json" \ -d "{\"title\":\"🤖 Aider ждёт approve\",\"message\":\"...\",\"priority\":9}" interact # передаём управление пользователю в ssh } eof}Что критично положить в push
Заголовок раздела «Что критично положить в push»- Что именно делаем. Команда, путь до файла, имя миграции, branch.
- Что произойдёт при
deny. Чтобы вы могли уверенно нажать «нет». - Diff / preview, если возможно. В extras можно положить ссылку на gist.
- Дедлайн. «Через 30 минут — abort», тогда из бара можно ничего не делать.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- Завершение долгой задачи — обратное: «уже сделал, иди ревьюить».
- WebScript — кнопка-callback без своего HTTP.
- WebSocket-протокол — чтобы агент моментально узнал, что вы нажали approve.