mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
- Rename platform/ to svc/ to avoid shadowing stdlib platform. - Replace from oclaw.* with from svc/runtime/interfaces; update -m CLI paths. - tests/conftest: prepend repo root to sys.path (no parent-folder package name). - CI: paths and offline_eval script under repo root. - Ops scripts: PYTHONPATH must be repo root for python -m runtime.* (fixes gateway/WhatsApp sidecar startup). - Fix default oclaw.json path in tabular/file attachment limits; stabilize attachment test config. Co-authored-by: Cursor <cursoragent@cursor.com>
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import os
|
|
import time
|
|
from typing import Any
|
|
|
|
from svc.files.file_attachments import process_file_data
|
|
|
|
PROTOCOL_VERSION = 3
|
|
MAX_PAYLOAD_BYTES = 26_214_400
|
|
MAX_BUFFERED_BYTES = 52_428_800
|
|
TICK_INTERVAL_MS = 15_000
|
|
PREAUTH_HANDSHAKE_TIMEOUT_MS = 15_000
|
|
WS_REQUIRE_AUTH = str(os.getenv("OCLAW_WS_REQUIRE_AUTH") or "1").strip().lower() not in ("0", "false", "no", "off")
|
|
WS_ALLOWED_ORIGINS = [s.strip() for s in str(os.getenv("OCLAW_WS_ALLOWED_ORIGINS") or "").split(",") if s.strip()]
|
|
WS_RATE_LIMIT_WINDOW_MS = int(os.getenv("OCLAW_WS_RATE_LIMIT_WINDOW_MS") or "60000")
|
|
WS_RATE_LIMIT_CONN_PER_WINDOW = int(os.getenv("OCLAW_WS_RATE_LIMIT_CONN_PER_WINDOW") or "120")
|
|
WS_RATE_LIMIT_IP_PER_WINDOW = int(os.getenv("OCLAW_WS_RATE_LIMIT_IP_PER_WINDOW") or "240")
|
|
WS_RATE_LIMIT_USER_PER_WINDOW = int(os.getenv("OCLAW_WS_RATE_LIMIT_USER_PER_WINDOW") or "360")
|
|
WS_SEND_QUEUE_MAX_MESSAGES = int(os.getenv("OCLAW_WS_SEND_QUEUE_MAX_MESSAGES") or "256")
|
|
WS_SEND_QUEUE_MAX_BYTES = int(os.getenv("OCLAW_WS_SEND_QUEUE_MAX_BYTES") or str(MAX_BUFFERED_BYTES))
|
|
WS_EVENT_REPLAY_MAX = int(os.getenv("OCLAW_WS_EVENT_REPLAY_MAX") or "256")
|
|
|
|
|
|
def now_ms() -> int:
|
|
return int(time.time() * 1000)
|
|
|
|
|
|
def error_shape(code: str, message: str, *, details: Any | None = None) -> dict[str, Any]:
|
|
out: dict[str, Any] = {"code": str(code or "INVALID_REQUEST"), "message": str(message or "invalid_request")}
|
|
if details is not None:
|
|
out["details"] = details
|
|
return out
|
|
|
|
|
|
def origin_is_allowed(origin: str | None, host: str | None) -> bool:
|
|
value = str(origin or "").strip()
|
|
if not value:
|
|
return True
|
|
allowlist = list(WS_ALLOWED_ORIGINS)
|
|
if allowlist:
|
|
return value in allowlist
|
|
host_value = str(host or "").strip()
|
|
if not host_value:
|
|
return False
|
|
lower = value.lower()
|
|
return lower.startswith(f"https://{host_value.lower()}") or lower.startswith(f"http://{host_value.lower()}")
|
|
|
|
|
|
def decode_base64_payload_ws(s: str | None) -> bytes | None:
|
|
raw = str(s or "").strip()
|
|
if not raw:
|
|
return None
|
|
if raw.startswith("data:") and "," in raw:
|
|
raw = raw.split(",", 1)[1].strip()
|
|
raw = raw.replace("-", "+").replace("_", "/")
|
|
pad = (-len(raw)) % 4
|
|
if pad:
|
|
raw += "=" * pad
|
|
try:
|
|
return base64.b64decode(raw, validate=False)
|
|
except Exception:
|
|
try:
|
|
return base64.standard_b64decode(raw)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def normalize_ws_attachments(raw: Any) -> list[dict[str, Any]]:
|
|
if not raw:
|
|
return []
|
|
items = raw if isinstance(raw, list) else []
|
|
out: list[dict[str, Any]] = []
|
|
for it in items:
|
|
if not isinstance(it, dict):
|
|
continue
|
|
if "type" in it:
|
|
out.append(it)
|
|
continue
|
|
name = str(it.get("name") or "file").strip() or "file"
|
|
b64 = it.get("data_base64") if "data_base64" in it else it.get("data")
|
|
if not isinstance(b64, str) or not b64.strip():
|
|
continue
|
|
data = decode_base64_payload_ws(b64)
|
|
if not data:
|
|
continue
|
|
got = process_file_data(name, data)
|
|
if got:
|
|
out.extend(got)
|
|
return out
|
|
|
|
|
|
__all__ = [
|
|
"PROTOCOL_VERSION",
|
|
"MAX_PAYLOAD_BYTES",
|
|
"MAX_BUFFERED_BYTES",
|
|
"TICK_INTERVAL_MS",
|
|
"PREAUTH_HANDSHAKE_TIMEOUT_MS",
|
|
"WS_REQUIRE_AUTH",
|
|
"WS_ALLOWED_ORIGINS",
|
|
"WS_RATE_LIMIT_WINDOW_MS",
|
|
"WS_RATE_LIMIT_CONN_PER_WINDOW",
|
|
"WS_RATE_LIMIT_IP_PER_WINDOW",
|
|
"WS_RATE_LIMIT_USER_PER_WINDOW",
|
|
"WS_SEND_QUEUE_MAX_MESSAGES",
|
|
"WS_SEND_QUEUE_MAX_BYTES",
|
|
"WS_EVENT_REPLAY_MAX",
|
|
"now_ms",
|
|
"error_shape",
|
|
"origin_is_allowed",
|
|
"decode_base64_payload_ws",
|
|
"normalize_ws_attachments",
|
|
]
|
|
|