mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-22 13:34:24 +00:00
144bec371c
* docs(platform): add HTTP Bot adapter design (RFC)
Standalone server-to-server HTTP adapter for driving a pipeline from external
systems (LangBot Space ticketing et al). Inbound via the existing unified
webhook route; outbound via signed callback POSTs. Preserves pipeline-native
N->1 aggregation and 1->M multi-reply without a long-lived WebSocket.
No core changes required (router/aggregator/pipeline untouched).
* feat(platform): add standalone HTTP Bot adapter
A first-class, vendor-neutral message-platform adapter (http_bot) for
server-to-server integrations (LangBot Space ticketing et al). Drives a
pipeline over plain HTTP with no long-lived connection:
- Inbound: signed POST to the existing unified webhook route /bots/<uuid>,
carrying a caller-defined session_id mapped to the LangBot launcher id via
get_launcher_id -> per-session isolation. Preserves pipeline-native N->1
aggregation for free.
- Outbound: each reply_message / reply_message_chunk becomes one signed
callback POST to the config-only callback_url, delivered in per-session
sequence order with retry/backoff -> 1->M multi-reply.
- Sub-paths: /reset (drop a session) and /sync (block for the collapsed reply).
- Auth: symmetric HMAC-SHA256 both directions (timestamp + replay window),
no JWT/Turnstile, no socket.
Decisions: callback URL is config-only (SSRF closed); reset + sync shipped;
Python + TS reference clients shipped (signing verified byte-identical 3-way).
No core changes: the unified webhook router, aggregator, query pool and
pipeline are untouched. Adapter is auto-discovered from platform/sources/.
Adds:
src/langbot/pkg/platform/sources/http_bot.{py,yaml,svg}
src/langbot/pkg/platform/sources/http_bot_signing.py
docs/platforms/http-bot.md, docs/http-bot-openapi.json
examples/http-bot/{client.py,client.ts,README.md}
Updates docs/HTTP_BOT_ADAPTER_DESIGN.md (status: implemented).
* docs(examples): add interactive HTTP Bot playground (browser debug console)
A single-file aiohttp web app (examples/http-bot/playground.py) that lets you
chat with a RUNNING http_bot bot from the browser and watch the protocol live:
signed inbound POST -> 202 ack -> 1->M signed callbacks streamed back via SSE,
with a debug panel showing the signature, HTTP status, and per-callback
sequence/verification. Light LangBot-styled UI.
On startup it reads the API key + http_bot bot from data/langbot.db and points
the bot's callback_url + secrets back at itself via the LangBot API (live
reload, no restart). README updated with a playground section.
* docs(examples): add Chinese README for http-bot reference clients
* style(platform): use </> code icon for http_bot adapter logo
* docs(examples): point http-bot guide links to docs.langbot.app
* style(platform): make http_bot icon a transparent monochrome </> so WebUI tints it like other adapters
* Revert to colorful </> badge for http_bot icon (WebUI renders it as-is)
96 lines
3.0 KiB
Python
96 lines
3.0 KiB
Python
"""HMAC signing utilities for the HTTP Bot adapter.
|
|
|
|
A dependency-free, symmetric HMAC-SHA256 scheme used in *both* directions:
|
|
|
|
signing_string = "{timestamp}." + raw_body_bytes
|
|
signature = "sha256=" + hex(HMAC_SHA256(secret, signing_string))
|
|
|
|
Inbound requests are signed by the caller and verified here; outbound
|
|
callbacks are signed here and verified by the caller. The scheme is trivial to
|
|
reproduce in any language (see docs/platforms/http-bot.md for JS/curl).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import time
|
|
|
|
# Header names (kept here so adapter + clients agree on a single source).
|
|
HEADER_TIMESTAMP = 'X-LB-Timestamp'
|
|
HEADER_SIGNATURE = 'X-LB-Signature'
|
|
HEADER_IDEMPOTENCY = 'X-LB-Idempotency-Key'
|
|
|
|
# Maximum allowed clock skew between signer and verifier (seconds).
|
|
DEFAULT_REPLAY_WINDOW = 300
|
|
|
|
|
|
def compute_signature(secret: str, body: bytes, timestamp: str | int) -> str:
|
|
"""Compute the ``sha256=<hex>`` signature for *body* at *timestamp*.
|
|
|
|
Args:
|
|
secret: Shared HMAC secret.
|
|
body: Raw request body bytes (exactly as sent on the wire).
|
|
timestamp: Unix timestamp (seconds) as str or int.
|
|
|
|
Returns:
|
|
The signature string, e.g. ``sha256=ab12...``.
|
|
"""
|
|
signing_string = f'{timestamp}.'.encode() + body
|
|
digest = hmac.new(secret.encode(), signing_string, hashlib.sha256).hexdigest()
|
|
return f'sha256={digest}'
|
|
|
|
|
|
def sign(secret: str, body: bytes, timestamp: int | None = None) -> tuple[str, str]:
|
|
"""Produce ``(timestamp, signature)`` for an outbound request.
|
|
|
|
Args:
|
|
secret: Shared HMAC secret.
|
|
body: Raw request body bytes.
|
|
timestamp: Optional fixed timestamp; defaults to ``int(time.time())``.
|
|
|
|
Returns:
|
|
``(timestamp_str, signature_str)``.
|
|
"""
|
|
ts = str(timestamp if timestamp is not None else int(time.time()))
|
|
return ts, compute_signature(secret, body, ts)
|
|
|
|
|
|
def verify(
|
|
secret: str,
|
|
body: bytes,
|
|
timestamp: str | None,
|
|
signature: str | None,
|
|
replay_window: int = DEFAULT_REPLAY_WINDOW,
|
|
) -> tuple[bool, str]:
|
|
"""Verify an inbound signature.
|
|
|
|
Args:
|
|
secret: Shared HMAC secret.
|
|
body: Raw request body bytes.
|
|
timestamp: Value of the timestamp header.
|
|
signature: Value of the signature header.
|
|
replay_window: Max allowed skew in seconds.
|
|
|
|
Returns:
|
|
``(ok, reason)``. ``reason`` is empty when ``ok`` is True, otherwise a
|
|
short machine-friendly cause (``missing_headers`` / ``bad_timestamp`` /
|
|
``expired`` / ``signature_mismatch``).
|
|
"""
|
|
if not timestamp or not signature:
|
|
return False, 'missing_headers'
|
|
|
|
try:
|
|
ts_int = int(float(timestamp))
|
|
except (ValueError, TypeError):
|
|
return False, 'bad_timestamp'
|
|
|
|
if abs(int(time.time()) - ts_int) > replay_window:
|
|
return False, 'expired'
|
|
|
|
expected = compute_signature(secret, body, timestamp)
|
|
if not hmac.compare_digest(expected, signature):
|
|
return False, 'signature_mismatch'
|
|
|
|
return True, ''
|