Files
hive/core/framework/host/webhook_server.py
T
Hundao 589c5b06fe fix: resolve all ruff lint and format errors across codebase (#7058)
- Auto-fixed 70 lint errors (import sorting, aliased errors, datetime.UTC)
- Fixed 85 remaining errors manually:
  - E501: wrapped long lines in queen_profiles, catalog, routes_credentials
  - F821: added missing TYPE_CHECKING imports for AgentHost, ToolRegistry,
    HookContext, HookResult; added runtime imports where needed
  - F811: removed duplicate method definitions in queen_lifecycle_tools
  - F841/B007: removed unused variables in discovery.py
  - W291: removed trailing whitespace in queen nodes
  - E402: moved import to top of queen_memory_v2.py
  - Fixed AgentRuntime -> AgentHost in example template type annotations
- Reformatted 343 files with ruff format
2026-04-16 19:30:01 +08:00

175 lines
5.3 KiB
Python

"""
Webhook HTTP Server - Receives HTTP requests and publishes them as EventBus events.
Only starts if webhook-type entry points are registered. Uses aiohttp for
a lightweight embedded HTTP server that runs within the existing asyncio loop.
"""
import hashlib
import hmac
import json
import logging
from dataclasses import dataclass
from aiohttp import web
from framework.host.event_bus import EventBus
logger = logging.getLogger(__name__)
@dataclass
class WebhookRoute:
"""A registered webhook route derived from an EntryPointSpec."""
source_id: str
path: str
methods: list[str]
secret: str | None = None # For HMAC-SHA256 signature verification
@dataclass
class WebhookServerConfig:
"""Configuration for the webhook HTTP server."""
host: str = "127.0.0.1"
port: int = 8080
class WebhookServer:
"""
Embedded HTTP server that receives webhook requests and publishes
them as WEBHOOK_RECEIVED events on the EventBus.
The server's only job is: receive HTTP -> publish AgentEvent.
Subscribers decide what to do with the event.
Lifecycle:
server = WebhookServer(event_bus, config)
server.add_route(WebhookRoute(...))
await server.start()
# ... server running ...
await server.stop()
"""
def __init__(
self,
event_bus: EventBus,
config: WebhookServerConfig | None = None,
):
self._event_bus = event_bus
self._config = config or WebhookServerConfig()
self._routes: dict[str, WebhookRoute] = {} # path -> route
self._app: web.Application | None = None
self._runner: web.AppRunner | None = None
self._site: web.TCPSite | None = None
def add_route(self, route: WebhookRoute) -> None:
"""Register a webhook route."""
self._routes[route.path] = route
async def start(self) -> None:
"""Start the HTTP server. No-op if no routes registered."""
if not self._routes:
logger.debug("No webhook routes registered, skipping server start")
return
self._app = web.Application()
for path, route in self._routes.items():
for method in route.methods:
self._app.router.add_route(method, path, self._handle_request)
self._runner = web.AppRunner(self._app)
await self._runner.setup()
self._site = web.TCPSite(
self._runner,
self._config.host,
self._config.port,
)
await self._site.start()
logger.info(
f"Webhook server started on {self._config.host}:{self._config.port} with {len(self._routes)} route(s)"
)
async def stop(self) -> None:
"""Stop the HTTP server gracefully."""
if self._runner:
await self._runner.cleanup()
self._runner = None
self._app = None
self._site = None
logger.info("Webhook server stopped")
async def _handle_request(self, request: web.Request) -> web.Response:
"""Handle an incoming webhook request."""
path = request.path
route = self._routes.get(path)
if route is None:
return web.json_response({"error": "Not found"}, status=404)
# Read body
try:
body = await request.read()
except Exception:
return web.json_response(
{"error": "Failed to read request body"},
status=400,
)
# Verify HMAC signature if secret is configured
if route.secret:
if not self._verify_signature(request, body, route.secret):
return web.json_response({"error": "Invalid signature"}, status=401)
# Parse body as JSON (fall back to raw text for non-JSON)
try:
payload = json.loads(body) if body else {}
except (json.JSONDecodeError, ValueError):
payload = {"raw_body": body.decode("utf-8", errors="replace")}
# Publish event to bus
await self._event_bus.emit_webhook_received(
source_id=route.source_id,
path=path,
method=request.method,
headers=dict(request.headers),
payload=payload,
query_params=dict(request.query),
)
return web.json_response({"status": "accepted"}, status=202)
def _verify_signature(
self,
request: web.Request,
body: bytes,
secret: str,
) -> bool:
"""Verify HMAC-SHA256 signature from X-Hub-Signature-256 header."""
signature_header = request.headers.get("X-Hub-Signature-256", "")
if not signature_header.startswith("sha256="):
return False
expected_sig = signature_header[7:] # strip "sha256="
computed_sig = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected_sig, computed_sig)
@property
def is_running(self) -> bool:
"""Check if the server is running."""
return self._site is not None
@property
def port(self) -> int | None:
"""Return the actual listening port (useful when configured with port=0)."""
if self._site and self._site._server and self._site._server.sockets:
return self._site._server.sockets[0].getsockname()[1]
return None