Merge branch 'master' into docs/backend-doc-pass

This commit is contained in:
Abhimanyu Saharan
2026-02-25 03:32:14 +05:30
committed by GitHub
344 changed files with 36956 additions and 3626 deletions

View File

@@ -49,10 +49,26 @@ class Settings(BaseSettings):
cors_origins: str = ""
base_url: str = ""
# Security response headers (blank disables header injection)
security_header_x_content_type_options: str = ""
security_header_x_frame_options: str = ""
security_header_referrer_policy: str = ""
security_header_permissions_policy: str = ""
# Database lifecycle
db_auto_migrate: bool = False
# RQ queueing / dispatch
rq_redis_url: str = "redis://localhost:6379/0"
rq_queue_name: str = "default"
rq_dispatch_throttle_seconds: float = 15.0
rq_dispatch_max_retries: int = 3
rq_dispatch_retry_base_seconds: float = 10.0
rq_dispatch_retry_max_seconds: float = 120.0
# OpenClaw gateway runtime compatibility
gateway_min_version: str = "2026.02.9"
# Logging
log_level: str = "INFO"
log_format: str = "text"

View File

@@ -224,12 +224,27 @@ def _get_request_id(request: Request) -> str | None:
def _error_payload(*, detail: object, request_id: str | None) -> dict[str, object]:
payload: dict[str, Any] = {"detail": detail}
payload: dict[str, Any] = {"detail": _json_safe(detail)}
if request_id:
payload["request_id"] = request_id
return payload
def _json_safe(value: object) -> object:
"""Return a JSON-serializable representation for error payloads."""
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
if isinstance(value, (bytearray, memoryview)):
return bytes(value).decode("utf-8", errors="replace")
if isinstance(value, dict):
return {str(key): _json_safe(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set)):
return [_json_safe(item) for item in value]
if value is None or isinstance(value, (str, int, float, bool)):
return value
return str(value)
async def _request_validation_handler(
request: Request,
exc: RequestValidationError,

View File

@@ -0,0 +1,81 @@
"""ASGI middleware for configurable security response headers."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: # pragma: no cover
from starlette.types import ASGIApp, Message, Receive, Scope, Send
class SecurityHeadersMiddleware:
"""Inject configured security headers into every HTTP response."""
_X_CONTENT_TYPE_OPTIONS = b"X-Content-Type-Options"
_X_FRAME_OPTIONS = b"X-Frame-Options"
_REFERRER_POLICY = b"Referrer-Policy"
_PERMISSIONS_POLICY = b"Permissions-Policy"
def __init__(
self,
app: ASGIApp,
*,
x_content_type_options: str = "",
x_frame_options: str = "",
referrer_policy: str = "",
permissions_policy: str = "",
) -> None:
self._app = app
self._configured_headers = self._build_configured_headers(
x_content_type_options=x_content_type_options,
x_frame_options=x_frame_options,
referrer_policy=referrer_policy,
permissions_policy=permissions_policy,
)
@classmethod
def _build_configured_headers(
cls,
*,
x_content_type_options: str,
x_frame_options: str,
referrer_policy: str,
permissions_policy: str,
) -> tuple[tuple[bytes, bytes, bytes], ...]:
configured: list[tuple[bytes, bytes, bytes]] = []
for header_name, value in (
(cls._X_CONTENT_TYPE_OPTIONS, x_content_type_options),
(cls._X_FRAME_OPTIONS, x_frame_options),
(cls._REFERRER_POLICY, referrer_policy),
(cls._PERMISSIONS_POLICY, permissions_policy),
):
normalized = value.strip()
if not normalized:
continue
configured.append(
(
header_name.lower(),
header_name,
normalized.encode("latin-1"),
)
)
return tuple(configured)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Append configured security headers unless already present."""
if scope["type"] != "http" or not self._configured_headers:
await self._app(scope, receive, send)
return
async def send_with_security_headers(message: Message) -> None:
if message["type"] == "http.response.start":
# Starlette uses `list[tuple[bytes, bytes]]` for raw headers.
headers: list[tuple[bytes, bytes]] = message.setdefault("headers", [])
existing = {key.lower() for key, _ in headers}
for key_lower, key, value in self._configured_headers:
if key_lower not in existing:
headers.append((key, value))
existing.add(key_lower)
await send(message)
await self._app(scope, receive, send_with_security_headers)