""" matrix-bridge-dagi — configuration and validation (M2.1 + M2.2 + M3.0) """ import os from dataclasses import dataclass, field from typing import FrozenSet @dataclass(frozen=True) class BridgeConfig: # Matrix homeserver matrix_homeserver_url: str matrix_access_token: str matrix_user_id: str # e.g. @dagi_bridge:daarion.space # Room → agent mapping (M1: single room) sofiia_room_id: str # e.g. !abcdef:daarion.space # DAGI backend dagi_gateway_url: str # e.g. http://dagi-gateway-node1:9300 default_node_id: str # e.g. NODA1 # Sofiia Console (audit write) sofiia_console_url: str # e.g. http://dagi-sofiia-console-node1:8002 sofiia_internal_token: str # X-Internal-Service-Token for audit ingest # Policy bridge_allowed_agents: FrozenSet[str] rate_limit_room_rpm: int # max messages per room per minute rate_limit_sender_rpm: int # max messages per sender per minute # H2: Backpressure queue queue_max_events: int # max pending items (drops oldest on full) worker_concurrency: int # parallel invoke workers queue_drain_timeout_s: float # graceful shutdown drain timeout # M2.1: Mixed rooms # "!roomX:server=sofiia,helion;!roomY:server=druid" bridge_mixed_room_map: str # "!roomX:server=helion" — explicit default per mixed room (optional) bridge_mixed_defaults: str # M2.2: Mixed room guard rails max_agents_per_mixed_room: int # fail-fast if room defines more agents than this max_slash_len: int # reject slash token longer than this (anti-garbage) unknown_agent_behavior: str # "ignore" | "reply_error" mixed_concurrency_cap: int # max parallel invokes per (room, agent); 0 = unlimited # M3.0: Operator control channel # "@ivan:daarion.space,@sergiy:daarion.space" bridge_operator_allowlist: str # "!opsroom:server,!opsroom2:server2" bridge_control_rooms: str # "ignore" | "reply_error" (send ⛔ to room on unauthorized attempt) control_unauthorized_behavior: str # Service identity node_id: str build_sha: str build_time: str def load_config() -> BridgeConfig: """Load and validate config from environment variables.""" def _require(key: str) -> str: v = os.getenv(key, "").strip() if not v: raise RuntimeError(f"Required env var {key!r} is not set") return v def _optional(key: str, default: str = "") -> str: return os.getenv(key, default).strip() allowed_raw = _optional("BRIDGE_ALLOWED_AGENTS", "sofiia") allowed = frozenset(a.strip() for a in allowed_raw.split(",") if a.strip()) return BridgeConfig( matrix_homeserver_url=_require("MATRIX_HOMESERVER_URL").rstrip("/"), matrix_access_token=_require("MATRIX_ACCESS_TOKEN"), matrix_user_id=_require("MATRIX_USER_ID"), sofiia_room_id=_require("SOFIIA_ROOM_ID"), dagi_gateway_url=_require("DAGI_GATEWAY_URL").rstrip("/"), default_node_id=_optional("DEFAULT_NODE_ID", "NODA1"), sofiia_console_url=_optional("SOFIIA_CONSOLE_URL", "").rstrip("/"), sofiia_internal_token=_optional("SOFIIA_INTERNAL_TOKEN", ""), bridge_allowed_agents=allowed, rate_limit_room_rpm=int(_optional("RATE_LIMIT_ROOM_RPM", "20")), rate_limit_sender_rpm=int(_optional("RATE_LIMIT_SENDER_RPM", "10")), queue_max_events=max(1, int(_optional("QUEUE_MAX_EVENTS", "100"))), worker_concurrency=max(1, int(_optional("WORKER_CONCURRENCY", "2"))), queue_drain_timeout_s=max(1.0, float(_optional("QUEUE_DRAIN_TIMEOUT_S", "5"))), bridge_mixed_room_map=_optional("BRIDGE_MIXED_ROOM_MAP", ""), bridge_mixed_defaults=_optional("BRIDGE_MIXED_DEFAULTS", ""), max_agents_per_mixed_room=max(1, int(_optional("MAX_AGENTS_PER_MIXED_ROOM", "5"))), max_slash_len=max(4, int(_optional("MAX_SLASH_LEN", "32"))), unknown_agent_behavior=_optional("UNKNOWN_AGENT_BEHAVIOR", "ignore"), mixed_concurrency_cap=max(0, int(_optional("MIXED_CONCURRENCY_CAP", "1"))), bridge_operator_allowlist=_optional("BRIDGE_OPERATOR_ALLOWLIST", ""), bridge_control_rooms=_optional("BRIDGE_CONTROL_ROOMS", ""), control_unauthorized_behavior=_optional("CONTROL_UNAUTHORIZED_BEHAVIOR", "ignore"), node_id=_optional("NODE_ID", "NODA1"), build_sha=_optional("BUILD_SHA", "dev"), build_time=_optional("BUILD_TIME", "local"), )