mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(gateway): align api_server and sms checkers with profile scope and startup guard
This commit is contained in:
parent
68dfeb4b16
commit
86f437509b
3 changed files with 1227 additions and 446 deletions
|
|
@ -277,6 +277,7 @@ class Platform(Enum):
|
||||||
``Platform("irc")`` works without modifying this enum. Dynamic members
|
``Platform("irc")`` works without modifying this enum. Dynamic members
|
||||||
are cached in ``_value2member_map_`` for identity-stable comparisons.
|
are cached in ``_value2member_map_`` for identity-stable comparisons.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
LOCAL = "local"
|
LOCAL = "local"
|
||||||
TELEGRAM = "telegram"
|
TELEGRAM = "telegram"
|
||||||
DISCORD = "discord"
|
DISCORD = "discord"
|
||||||
|
|
@ -301,6 +302,7 @@ class Platform(Enum):
|
||||||
QQBOT = "qqbot"
|
QQBOT = "qqbot"
|
||||||
YUANBAO = "yuanbao"
|
YUANBAO = "yuanbao"
|
||||||
RELAY = "relay" # generic relay adapter fronted by the connector (EXPERIMENTAL)
|
RELAY = "relay" # generic relay adapter fronted by the connector (EXPERIMENTAL)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _missing_(cls, value):
|
def _missing_(cls, value):
|
||||||
"""Accept unknown platform names only for known plugin adapters.
|
"""Accept unknown platform names only for known plugin adapters.
|
||||||
|
|
@ -334,6 +336,7 @@ class Platform(Enum):
|
||||||
# the enum was defined).
|
# the enum was defined).
|
||||||
try:
|
try:
|
||||||
from gateway.platform_registry import platform_registry
|
from gateway.platform_registry import platform_registry
|
||||||
|
|
||||||
if platform_registry.is_registered(value):
|
if platform_registry.is_registered(value):
|
||||||
pseudo = object.__new__(cls)
|
pseudo = object.__new__(cls)
|
||||||
pseudo._value_ = value
|
pseudo._value_ = value
|
||||||
|
|
@ -421,17 +424,18 @@ def platform_binds_port(platform_value: str, extra: Optional[dict] = None) -> bo
|
||||||
class HomeChannel:
|
class HomeChannel:
|
||||||
"""
|
"""
|
||||||
Default destination for a platform.
|
Default destination for a platform.
|
||||||
|
|
||||||
When a cron job specifies deliver="telegram" without a specific chat ID,
|
When a cron job specifies deliver="telegram" without a specific chat ID,
|
||||||
messages are sent to this home channel. Thread-aware platforms may also
|
messages are sent to this home channel. Thread-aware platforms may also
|
||||||
store a thread/topic ID so the bare platform target routes to the exact
|
store a thread/topic ID so the bare platform target routes to the exact
|
||||||
conversation where /sethome was run.
|
conversation where /sethome was run.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
platform: Platform
|
platform: Platform
|
||||||
chat_id: str
|
chat_id: str
|
||||||
name: str # Human-readable name for display
|
name: str # Human-readable name for display
|
||||||
thread_id: Optional[str] = None
|
thread_id: Optional[str] = None
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
result = {
|
result = {
|
||||||
"platform": self.platform.value,
|
"platform": self.platform.value,
|
||||||
|
|
@ -441,7 +445,7 @@ class HomeChannel:
|
||||||
if self.thread_id:
|
if self.thread_id:
|
||||||
result["thread_id"] = self.thread_id
|
result["thread_id"] = self.thread_id
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> "HomeChannel":
|
def from_dict(cls, data: Dict[str, Any]) -> "HomeChannel":
|
||||||
return cls(
|
return cls(
|
||||||
|
|
@ -456,7 +460,7 @@ class HomeChannel:
|
||||||
class SessionResetPolicy:
|
class SessionResetPolicy:
|
||||||
"""
|
"""
|
||||||
Controls when sessions reset (lose context).
|
Controls when sessions reset (lose context).
|
||||||
|
|
||||||
Modes:
|
Modes:
|
||||||
- "daily": Reset at a specific hour each day
|
- "daily": Reset at a specific hour each day
|
||||||
- "idle": Reset after N minutes of inactivity
|
- "idle": Reset after N minutes of inactivity
|
||||||
|
|
@ -468,11 +472,15 @@ class SessionResetPolicy:
|
||||||
overrides). Changed July 2026 from "both" (24h idle + daily 4am), which
|
overrides). Changed July 2026 from "both" (24h idle + daily 4am), which
|
||||||
surprised users who expected their conversations to persist.
|
surprised users who expected their conversations to persist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mode: str = "none" # "daily", "idle", "both", or "none"
|
mode: str = "none" # "daily", "idle", "both", or "none"
|
||||||
at_hour: int = 4 # Hour for daily reset (0-23, local time)
|
at_hour: int = 4 # Hour for daily reset (0-23, local time)
|
||||||
idle_minutes: int = 1440 # Minutes of inactivity before reset (24 hours)
|
idle_minutes: int = 1440 # Minutes of inactivity before reset (24 hours)
|
||||||
notify: bool = True # Send a notification to the user when auto-reset occurs
|
notify: bool = True # Send a notification to the user when auto-reset occurs
|
||||||
notify_exclude_platforms: tuple = ("api_server", "webhook") # Platforms that don't get reset notifications
|
notify_exclude_platforms: tuple = (
|
||||||
|
"api_server",
|
||||||
|
"webhook",
|
||||||
|
) # Platforms that don't get reset notifications
|
||||||
# A background process this many hours old (or older) no longer blocks
|
# A background process this many hours old (or older) no longer blocks
|
||||||
# session idle/daily reset. A forgotten preview server should not keep a
|
# session idle/daily reset. A forgotten preview server should not keep a
|
||||||
# session alive forever (#29177). The process is NOT killed — only ignored
|
# session alive forever (#29177). The process is NOT killed — only ignored
|
||||||
|
|
@ -489,7 +497,7 @@ class SessionResetPolicy:
|
||||||
"notify_exclude_platforms": list(self.notify_exclude_platforms),
|
"notify_exclude_platforms": list(self.notify_exclude_platforms),
|
||||||
"bg_process_max_age_hours": self.bg_process_max_age_hours,
|
"bg_process_max_age_hours": self.bg_process_max_age_hours,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> "SessionResetPolicy":
|
def from_dict(cls, data: Dict[str, Any]) -> "SessionResetPolicy":
|
||||||
data = _coerce_dict(data)
|
data = _coerce_dict(data)
|
||||||
|
|
@ -505,7 +513,9 @@ class SessionResetPolicy:
|
||||||
at_hour=at_hour if at_hour is not None else 4,
|
at_hour=at_hour if at_hour is not None else 4,
|
||||||
idle_minutes=idle_minutes if idle_minutes is not None else 1440,
|
idle_minutes=idle_minutes if idle_minutes is not None else 1440,
|
||||||
notify=_coerce_bool(notify, True),
|
notify=_coerce_bool(notify, True),
|
||||||
notify_exclude_platforms=tuple(exclude) if exclude is not None else ("api_server", "webhook"),
|
notify_exclude_platforms=tuple(exclude)
|
||||||
|
if exclude is not None
|
||||||
|
else ("api_server", "webhook"),
|
||||||
bg_process_max_age_hours=bg_max_age if bg_max_age is not None else 24,
|
bg_process_max_age_hours=bg_max_age if bg_max_age is not None else 24,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -519,6 +529,7 @@ class ChannelOverride:
|
||||||
Enables different channels (e.g. Discord #daily vs #dev) to use different
|
Enables different channels (e.g. Discord #daily vs #dev) to use different
|
||||||
models and personas without running separate gateway instances.
|
models and personas without running separate gateway instances.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
provider: Optional[str] = None
|
provider: Optional[str] = None
|
||||||
system_prompt: Optional[str] = None
|
system_prompt: Optional[str] = None
|
||||||
|
|
@ -563,6 +574,7 @@ PLATFORM_TOKEN_ENV_NAMES: dict["Platform", str] = {
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlatformConfig:
|
class PlatformConfig:
|
||||||
"""Configuration for a single messaging platform."""
|
"""Configuration for a single messaging platform."""
|
||||||
|
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
token: Optional[str] = None # Bot token (Telegram, Discord)
|
token: Optional[str] = None # Bot token (Telegram, Discord)
|
||||||
api_key: Optional[str] = None # API key if different from token
|
api_key: Optional[str] = None # API key if different from token
|
||||||
|
|
@ -690,6 +702,7 @@ DEFAULT_STREAMING_CURSOR: str = " ▉"
|
||||||
@dataclass
|
@dataclass
|
||||||
class StreamingConfig:
|
class StreamingConfig:
|
||||||
"""Configuration for real-time token streaming to messaging platforms."""
|
"""Configuration for real-time token streaming to messaging platforms."""
|
||||||
|
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
# Transport selection:
|
# Transport selection:
|
||||||
# "auto" — prefer native streaming-draft updates when the platform
|
# "auto" — prefer native streaming-draft updates when the platform
|
||||||
|
|
@ -770,10 +783,12 @@ class StreamingConfig:
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
transport=transport,
|
transport=transport,
|
||||||
edit_interval=_coerce_float(
|
edit_interval=_coerce_float(
|
||||||
data.get("edit_interval"), DEFAULT_STREAMING_EDIT_INTERVAL,
|
data.get("edit_interval"),
|
||||||
|
DEFAULT_STREAMING_EDIT_INTERVAL,
|
||||||
),
|
),
|
||||||
buffer_threshold=_coerce_int(
|
buffer_threshold=_coerce_int(
|
||||||
data.get("buffer_threshold"), DEFAULT_STREAMING_BUFFER_THRESHOLD,
|
data.get("buffer_threshold"),
|
||||||
|
DEFAULT_STREAMING_BUFFER_THRESHOLD,
|
||||||
),
|
),
|
||||||
cursor=data.get("cursor", DEFAULT_STREAMING_CURSOR),
|
cursor=data.get("cursor", DEFAULT_STREAMING_CURSOR),
|
||||||
fresh_final_after_seconds=_coerce_float(
|
fresh_final_after_seconds=_coerce_float(
|
||||||
|
|
@ -789,6 +804,20 @@ class StreamingConfig:
|
||||||
# platform is sufficiently configured to be considered "connected". Platforms
|
# platform is sufficiently configured to be considered "connected". Platforms
|
||||||
# that rely on the generic ``token or api_key`` check (Telegram, Discord,
|
# that rely on the generic ``token or api_key`` check (Telegram, Discord,
|
||||||
# Slack, Matrix, Mattermost, HomeAssistant) do not need an entry here.
|
# Slack, Matrix, Mattermost, HomeAssistant) do not need an entry here.
|
||||||
|
def _check_api_server(cfg: PlatformConfig) -> bool:
|
||||||
|
if not cfg:
|
||||||
|
return False
|
||||||
|
key = cfg.extra.get("key")
|
||||||
|
if not key:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
from hermes_cli.auth import has_usable_secret
|
||||||
|
|
||||||
|
return has_usable_secret(key, min_length=16)
|
||||||
|
except ImportError:
|
||||||
|
return len(str(key).strip()) >= 16
|
||||||
|
|
||||||
|
|
||||||
_PLATFORM_CONNECTED_CHECKERS: dict[Platform, Callable[[PlatformConfig], bool]] = {
|
_PLATFORM_CONNECTED_CHECKERS: dict[Platform, Callable[[PlatformConfig], bool]] = {
|
||||||
Platform.WEIXIN: lambda cfg: bool(
|
Platform.WEIXIN: lambda cfg: bool(
|
||||||
cfg.extra.get("account_id") and (cfg.token or cfg.extra.get("token"))
|
cfg.extra.get("account_id") and (cfg.token or cfg.extra.get("token"))
|
||||||
|
|
@ -798,8 +827,8 @@ _PLATFORM_CONNECTED_CHECKERS: dict[Platform, Callable[[PlatformConfig], bool]] =
|
||||||
),
|
),
|
||||||
Platform.SIGNAL: lambda cfg: bool(cfg.extra.get("http_url")),
|
Platform.SIGNAL: lambda cfg: bool(cfg.extra.get("http_url")),
|
||||||
Platform.EMAIL: lambda cfg: bool(cfg.extra.get("address")),
|
Platform.EMAIL: lambda cfg: bool(cfg.extra.get("address")),
|
||||||
Platform.SMS: lambda cfg: bool(os.getenv("TWILIO_ACCOUNT_SID")),
|
Platform.SMS: lambda cfg: bool(_getenv("TWILIO_ACCOUNT_SID")),
|
||||||
Platform.API_SERVER: lambda cfg: bool(cfg.extra.get("key")) if cfg else False,
|
Platform.API_SERVER: _check_api_server,
|
||||||
Platform.WEBHOOK: lambda cfg: True,
|
Platform.WEBHOOK: lambda cfg: True,
|
||||||
Platform.MSGRAPH_WEBHOOK: lambda cfg: bool(
|
Platform.MSGRAPH_WEBHOOK: lambda cfg: bool(
|
||||||
str(cfg.extra.get("client_state") or "").strip()
|
str(cfg.extra.get("client_state") or "").strip()
|
||||||
|
|
@ -827,23 +856,24 @@ _PLATFORM_CONNECTED_CHECKERS: dict[Platform, Callable[[PlatformConfig], bool]] =
|
||||||
class GatewayConfig:
|
class GatewayConfig:
|
||||||
"""
|
"""
|
||||||
Main gateway configuration.
|
Main gateway configuration.
|
||||||
|
|
||||||
Manages all platform connections, session policies, and delivery settings.
|
Manages all platform connections, session policies, and delivery settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Platform configurations
|
# Platform configurations
|
||||||
platforms: Dict[Platform, PlatformConfig] = field(default_factory=dict)
|
platforms: Dict[Platform, PlatformConfig] = field(default_factory=dict)
|
||||||
|
|
||||||
# Session reset policies by type
|
# Session reset policies by type
|
||||||
default_reset_policy: SessionResetPolicy = field(default_factory=SessionResetPolicy)
|
default_reset_policy: SessionResetPolicy = field(default_factory=SessionResetPolicy)
|
||||||
reset_by_type: Dict[str, SessionResetPolicy] = field(default_factory=dict)
|
reset_by_type: Dict[str, SessionResetPolicy] = field(default_factory=dict)
|
||||||
reset_by_platform: Dict[Platform, SessionResetPolicy] = field(default_factory=dict)
|
reset_by_platform: Dict[Platform, SessionResetPolicy] = field(default_factory=dict)
|
||||||
|
|
||||||
# Reset trigger commands
|
# Reset trigger commands
|
||||||
reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"])
|
reset_triggers: List[str] = field(default_factory=lambda: ["/new", "/reset"])
|
||||||
|
|
||||||
# User-defined quick commands (slash commands that bypass the agent loop)
|
# User-defined quick commands (slash commands that bypass the agent loop)
|
||||||
quick_commands: Dict[str, Any] = field(default_factory=dict)
|
quick_commands: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
# Storage paths
|
# Storage paths
|
||||||
sessions_dir: Path = field(default_factory=lambda: get_hermes_home() / "sessions")
|
sessions_dir: Path = field(default_factory=lambda: get_hermes_home() / "sessions")
|
||||||
|
|
||||||
|
|
@ -853,7 +883,7 @@ class GatewayConfig:
|
||||||
# tooling and downgrade safety; set gateway.write_sessions_json: false in
|
# tooling and downgrade safety; set gateway.write_sessions_json: false in
|
||||||
# config.yaml to stop producing the file.
|
# config.yaml to stop producing the file.
|
||||||
write_sessions_json: bool = True
|
write_sessions_json: bool = True
|
||||||
|
|
||||||
# Delivery settings
|
# Delivery settings
|
||||||
always_log_local: bool = True # Always save cron outputs to local files
|
always_log_local: bool = True # Always save cron outputs to local files
|
||||||
# Drop outbound "silence narration" messages (e.g. *(silent)*, 🔇, a bare
|
# Drop outbound "silence narration" messages (e.g. *(silent)*, 🔇, a bare
|
||||||
|
|
@ -866,12 +896,18 @@ class GatewayConfig:
|
||||||
|
|
||||||
# STT settings
|
# STT settings
|
||||||
stt_enabled: bool = True # Whether to auto-transcribe inbound voice messages
|
stt_enabled: bool = True # Whether to auto-transcribe inbound voice messages
|
||||||
stt_echo_transcripts: bool = True # Whether to echo raw STT transcripts back to the user
|
stt_echo_transcripts: bool = (
|
||||||
|
True # Whether to echo raw STT transcripts back to the user
|
||||||
|
)
|
||||||
|
|
||||||
# Session isolation in shared chats
|
# Session isolation in shared chats
|
||||||
group_sessions_per_user: bool = True # Isolate group/channel sessions per participant when user IDs are available
|
group_sessions_per_user: bool = True # Isolate group/channel sessions per participant when user IDs are available
|
||||||
thread_sessions_per_user: bool = False # When False (default), threads are shared across all participants
|
thread_sessions_per_user: bool = (
|
||||||
max_concurrent_sessions: Optional[int] = None # Positive int caps simultaneous active chat sessions
|
False # When False (default), threads are shared across all participants
|
||||||
|
)
|
||||||
|
max_concurrent_sessions: Optional[int] = (
|
||||||
|
None # Positive int caps simultaneous active chat sessions
|
||||||
|
)
|
||||||
|
|
||||||
# Multi-profile multiplexing (opt-in; default off preserves one-gateway-per-profile).
|
# Multi-profile multiplexing (opt-in; default off preserves one-gateway-per-profile).
|
||||||
# When True, the default profile's gateway serves inbound messages for every
|
# When True, the default profile's gateway serves inbound messages for every
|
||||||
|
|
@ -924,7 +960,9 @@ class GatewayConfig:
|
||||||
connected.append(platform)
|
connected.append(platform)
|
||||||
return sorted(connected, key=lambda p: str(p.value))
|
return sorted(connected, key=lambda p: str(p.value))
|
||||||
|
|
||||||
def _is_platform_connected(self, platform: Platform, config: PlatformConfig) -> bool:
|
def _is_platform_connected(
|
||||||
|
self, platform: Platform, config: PlatformConfig
|
||||||
|
) -> bool:
|
||||||
"""Check whether a single platform is sufficiently configured."""
|
"""Check whether a single platform is sufficiently configured."""
|
||||||
# Weixin requires both a token and an account_id (checked first so
|
# Weixin requires both a token and an account_id (checked first so
|
||||||
# the generic token branch doesn't let it through without account_id).
|
# the generic token branch doesn't let it through without account_id).
|
||||||
|
|
@ -949,8 +987,10 @@ class GatewayConfig:
|
||||||
# discovery in the normal path). discover_plugins() is idempotent.
|
# discovery in the normal path). discover_plugins() is idempotent.
|
||||||
try:
|
try:
|
||||||
from gateway.platform_registry import platform_registry
|
from gateway.platform_registry import platform_registry
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from hermes_cli.plugins import discover_plugins
|
from hermes_cli.plugins import discover_plugins
|
||||||
|
|
||||||
discover_plugins()
|
discover_plugins()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -965,43 +1005,37 @@ class GatewayConfig:
|
||||||
pass # Registry not yet initialised during early import
|
pass # Registry not yet initialised during early import
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_home_channel(self, platform: Platform) -> Optional[HomeChannel]:
|
def get_home_channel(self, platform: Platform) -> Optional[HomeChannel]:
|
||||||
"""Get the home channel for a platform."""
|
"""Get the home channel for a platform."""
|
||||||
config = self.platforms.get(platform)
|
config = self.platforms.get(platform)
|
||||||
if config:
|
if config:
|
||||||
return config.home_channel
|
return config.home_channel
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_reset_policy(
|
def get_reset_policy(
|
||||||
self,
|
self, platform: Optional[Platform] = None, session_type: Optional[str] = None
|
||||||
platform: Optional[Platform] = None,
|
|
||||||
session_type: Optional[str] = None
|
|
||||||
) -> SessionResetPolicy:
|
) -> SessionResetPolicy:
|
||||||
"""
|
"""
|
||||||
Get the appropriate reset policy for a session.
|
Get the appropriate reset policy for a session.
|
||||||
|
|
||||||
Priority: platform override > type override > default
|
Priority: platform override > type override > default
|
||||||
"""
|
"""
|
||||||
# Platform-specific override takes precedence
|
# Platform-specific override takes precedence
|
||||||
if platform and platform in self.reset_by_platform:
|
if platform and platform in self.reset_by_platform:
|
||||||
return self.reset_by_platform[platform]
|
return self.reset_by_platform[platform]
|
||||||
|
|
||||||
# Type-specific override (dm, group, thread)
|
# Type-specific override (dm, group, thread)
|
||||||
if session_type and session_type in self.reset_by_type:
|
if session_type and session_type in self.reset_by_type:
|
||||||
return self.reset_by_type[session_type]
|
return self.reset_by_type[session_type]
|
||||||
|
|
||||||
return self.default_reset_policy
|
return self.default_reset_policy
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"platforms": {
|
"platforms": {p.value: c.to_dict() for p, c in self.platforms.items()},
|
||||||
p.value: c.to_dict() for p, c in self.platforms.items()
|
|
||||||
},
|
|
||||||
"default_reset_policy": self.default_reset_policy.to_dict(),
|
"default_reset_policy": self.default_reset_policy.to_dict(),
|
||||||
"reset_by_type": {
|
"reset_by_type": {k: v.to_dict() for k, v in self.reset_by_type.items()},
|
||||||
k: v.to_dict() for k, v in self.reset_by_type.items()
|
|
||||||
},
|
|
||||||
"reset_by_platform": {
|
"reset_by_platform": {
|
||||||
p.value: v.to_dict() for p, v in self.reset_by_platform.items()
|
p.value: v.to_dict() for p, v in self.reset_by_platform.items()
|
||||||
},
|
},
|
||||||
|
|
@ -1026,7 +1060,7 @@ class GatewayConfig:
|
||||||
for r in self.profile_routes
|
for r in self.profile_routes
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> "GatewayConfig":
|
def from_dict(cls, data: Dict[str, Any]) -> "GatewayConfig":
|
||||||
data = _coerce_dict(data)
|
data = _coerce_dict(data)
|
||||||
|
|
@ -1040,34 +1074,42 @@ class GatewayConfig:
|
||||||
platforms[platform] = PlatformConfig.from_dict(platform_data)
|
platforms[platform] = PlatformConfig.from_dict(platform_data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass # Skip unknown platforms
|
pass # Skip unknown platforms
|
||||||
|
|
||||||
reset_by_type = {}
|
reset_by_type = {}
|
||||||
for type_name, policy_data in _coerce_dict(data.get("reset_by_type", {})).items():
|
for type_name, policy_data in _coerce_dict(
|
||||||
|
data.get("reset_by_type", {})
|
||||||
|
).items():
|
||||||
reset_by_type[type_name] = SessionResetPolicy.from_dict(policy_data)
|
reset_by_type[type_name] = SessionResetPolicy.from_dict(policy_data)
|
||||||
|
|
||||||
reset_by_platform = {}
|
reset_by_platform = {}
|
||||||
for platform_name, policy_data in _coerce_dict(data.get("reset_by_platform", {})).items():
|
for platform_name, policy_data in _coerce_dict(
|
||||||
|
data.get("reset_by_platform", {})
|
||||||
|
).items():
|
||||||
try:
|
try:
|
||||||
platform = Platform(platform_name)
|
platform = Platform(platform_name)
|
||||||
reset_by_platform[platform] = SessionResetPolicy.from_dict(policy_data)
|
reset_by_platform[platform] = SessionResetPolicy.from_dict(policy_data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
default_policy = SessionResetPolicy()
|
default_policy = SessionResetPolicy()
|
||||||
if "default_reset_policy" in data:
|
if "default_reset_policy" in data:
|
||||||
default_policy = SessionResetPolicy.from_dict(data["default_reset_policy"])
|
default_policy = SessionResetPolicy.from_dict(data["default_reset_policy"])
|
||||||
|
|
||||||
sessions_dir = get_hermes_home() / "sessions"
|
sessions_dir = get_hermes_home() / "sessions"
|
||||||
if "sessions_dir" in data:
|
if "sessions_dir" in data:
|
||||||
sessions_dir = Path(data["sessions_dir"])
|
sessions_dir = Path(data["sessions_dir"])
|
||||||
|
|
||||||
quick_commands = data.get("quick_commands", {})
|
quick_commands = data.get("quick_commands", {})
|
||||||
if not isinstance(quick_commands, dict):
|
if not isinstance(quick_commands, dict):
|
||||||
quick_commands = {}
|
quick_commands = {}
|
||||||
|
|
||||||
stt_enabled = data.get("stt_enabled")
|
stt_enabled = data.get("stt_enabled")
|
||||||
if stt_enabled is None:
|
if stt_enabled is None:
|
||||||
stt_enabled = data.get("stt", {}).get("enabled") if isinstance(data.get("stt"), dict) else None
|
stt_enabled = (
|
||||||
|
data.get("stt", {}).get("enabled")
|
||||||
|
if isinstance(data.get("stt"), dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
stt_echo_transcripts = data.get("stt_echo_transcripts")
|
stt_echo_transcripts = data.get("stt_echo_transcripts")
|
||||||
if stt_echo_transcripts is None:
|
if stt_echo_transcripts is None:
|
||||||
stt_echo_transcripts = (
|
stt_echo_transcripts = (
|
||||||
|
|
@ -1079,7 +1121,9 @@ class GatewayConfig:
|
||||||
group_sessions_per_user = data.get("group_sessions_per_user")
|
group_sessions_per_user = data.get("group_sessions_per_user")
|
||||||
thread_sessions_per_user = data.get("thread_sessions_per_user")
|
thread_sessions_per_user = data.get("thread_sessions_per_user")
|
||||||
multiplex_profiles = data.get("multiplex_profiles")
|
multiplex_profiles = data.get("multiplex_profiles")
|
||||||
nested_gateway = data.get("gateway") if isinstance(data.get("gateway"), dict) else {}
|
nested_gateway = (
|
||||||
|
data.get("gateway") if isinstance(data.get("gateway"), dict) else {}
|
||||||
|
)
|
||||||
if "systemd_watchdog_seconds" in data:
|
if "systemd_watchdog_seconds" in data:
|
||||||
systemd_watchdog_raw = data.get("systemd_watchdog_seconds")
|
systemd_watchdog_raw = data.get("systemd_watchdog_seconds")
|
||||||
systemd_watchdog_key = "systemd_watchdog_seconds"
|
systemd_watchdog_key = "systemd_watchdog_seconds"
|
||||||
|
|
@ -1128,6 +1172,7 @@ class GatewayConfig:
|
||||||
|
|
||||||
# Parse profile routes (validated by gateway.profile_routing)
|
# Parse profile routes (validated by gateway.profile_routing)
|
||||||
from gateway.profile_routing import parse_profile_routes
|
from gateway.profile_routing import parse_profile_routes
|
||||||
|
|
||||||
profile_routes = parse_profile_routes(data.get("profile_routes") or [])
|
profile_routes = parse_profile_routes(data.get("profile_routes") or [])
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
|
|
@ -1216,6 +1261,7 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
# Primary source: config.yaml
|
# Primary source: config.yaml
|
||||||
try:
|
try:
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
config_yaml_path = _home / "config.yaml"
|
config_yaml_path = _home / "config.yaml"
|
||||||
if config_yaml_path.exists():
|
if config_yaml_path.exists():
|
||||||
with open(config_yaml_path, encoding="utf-8") as f:
|
with open(config_yaml_path, encoding="utf-8") as f:
|
||||||
|
|
@ -1227,6 +1273,7 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
# session_reset / quick_commands / stt / model would be ignored by
|
# session_reset / quick_commands / stt / model would be ignored by
|
||||||
# the messaging gateway. Fail-open via the shared helper.
|
# the messaging gateway. Fail-open via the shared helper.
|
||||||
from hermes_cli import managed_scope
|
from hermes_cli import managed_scope
|
||||||
|
|
||||||
yaml_cfg = managed_scope.apply_managed_overlay(yaml_cfg)
|
yaml_cfg = managed_scope.apply_managed_overlay(yaml_cfg)
|
||||||
|
|
||||||
# Shared nested-fallback source: settings meant to be top-level
|
# Shared nested-fallback source: settings meant to be top-level
|
||||||
|
|
@ -1300,11 +1347,18 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
gw_data["profile_routes"] = _pr
|
gw_data["profile_routes"] = _pr
|
||||||
|
|
||||||
if isinstance(gateway_section, dict):
|
if isinstance(gateway_section, dict):
|
||||||
if "multiplex_profiles" in gateway_section and "multiplex_profiles" not in gw_data:
|
if (
|
||||||
|
"multiplex_profiles" in gateway_section
|
||||||
|
and "multiplex_profiles" not in gw_data
|
||||||
|
):
|
||||||
# gateway.multiplex_profiles written by `hermes config set gateway.multiplex_profiles true`
|
# gateway.multiplex_profiles written by `hermes config set gateway.multiplex_profiles true`
|
||||||
gw_data["multiplex_profiles"] = gateway_section["multiplex_profiles"]
|
gw_data["multiplex_profiles"] = gateway_section[
|
||||||
|
"multiplex_profiles"
|
||||||
|
]
|
||||||
if "max_concurrent_sessions" in gateway_section:
|
if "max_concurrent_sessions" in gateway_section:
|
||||||
gw_data["max_concurrent_sessions"] = gateway_section["max_concurrent_sessions"]
|
gw_data["max_concurrent_sessions"] = gateway_section[
|
||||||
|
"max_concurrent_sessions"
|
||||||
|
]
|
||||||
if "systemd_watchdog_seconds" in gateway_section:
|
if "systemd_watchdog_seconds" in gateway_section:
|
||||||
gw_data["systemd_watchdog_seconds"] = gateway_section[
|
gw_data["systemd_watchdog_seconds"] = gateway_section[
|
||||||
"systemd_watchdog_seconds"
|
"systemd_watchdog_seconds"
|
||||||
|
|
@ -1348,9 +1402,11 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
]
|
]
|
||||||
|
|
||||||
if "unauthorized_dm_behavior" in yaml_cfg:
|
if "unauthorized_dm_behavior" in yaml_cfg:
|
||||||
gw_data["unauthorized_dm_behavior"] = _normalize_unauthorized_dm_behavior(
|
gw_data["unauthorized_dm_behavior"] = (
|
||||||
yaml_cfg.get("unauthorized_dm_behavior"),
|
_normalize_unauthorized_dm_behavior(
|
||||||
"pair",
|
yaml_cfg.get("unauthorized_dm_behavior"),
|
||||||
|
"pair",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
elif isinstance(gateway_section, dict) and "unauthorized_dm_behavior" in gateway_section:
|
elif isinstance(gateway_section, dict) and "unauthorized_dm_behavior" in gateway_section:
|
||||||
gw_data["unauthorized_dm_behavior"] = _normalize_unauthorized_dm_behavior(
|
gw_data["unauthorized_dm_behavior"] = _normalize_unauthorized_dm_behavior(
|
||||||
|
|
@ -1362,7 +1418,9 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
# ``gateway.platforms`` are loaded the same way as top-level
|
# ``gateway.platforms`` are loaded the same way as top-level
|
||||||
# ``platforms``. Merge nested first so top-level config keeps
|
# ``platforms``. Merge nested first so top-level config keeps
|
||||||
# precedence, matching the existing gateway.streaming fallback.
|
# precedence, matching the existing gateway.streaming fallback.
|
||||||
gateway_platforms = gateway_cfg.get("platforms") if isinstance(gateway_cfg, dict) else None
|
gateway_platforms = (
|
||||||
|
gateway_cfg.get("platforms") if isinstance(gateway_cfg, dict) else None
|
||||||
|
)
|
||||||
platforms_data = gw_data.setdefault("platforms", {})
|
platforms_data = gw_data.setdefault("platforms", {})
|
||||||
if not isinstance(platforms_data, dict):
|
if not isinstance(platforms_data, dict):
|
||||||
platforms_data = {}
|
platforms_data = {}
|
||||||
|
|
@ -1378,7 +1436,10 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
if not isinstance(existing, dict):
|
if not isinstance(existing, dict):
|
||||||
existing = {}
|
existing = {}
|
||||||
# Deep-merge extra dicts so gateway.json defaults survive
|
# Deep-merge extra dicts so gateway.json defaults survive
|
||||||
merged_extra = {**existing.get("extra", {}), **plat_block.get("extra", {})}
|
merged_extra = {
|
||||||
|
**existing.get("extra", {}),
|
||||||
|
**plat_block.get("extra", {}),
|
||||||
|
}
|
||||||
if "enabled" in plat_block:
|
if "enabled" in plat_block:
|
||||||
merged_extra["_enabled_explicit"] = True
|
merged_extra["_enabled_explicit"] = True
|
||||||
merged = {**existing, **plat_block}
|
merged = {**existing, **plat_block}
|
||||||
|
|
@ -1394,6 +1455,7 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
# so plugin authors get the same shared-key bridging (#24836).
|
# so plugin authors get the same shared-key bridging (#24836).
|
||||||
try:
|
try:
|
||||||
from hermes_cli.plugins import discover_plugins
|
from hermes_cli.plugins import discover_plugins
|
||||||
|
|
||||||
discover_plugins() # idempotent
|
discover_plugins() # idempotent
|
||||||
from gateway.platform_registry import platform_registry as _pr
|
from gateway.platform_registry import platform_registry as _pr
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1438,9 +1500,11 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
# Collect bridgeable keys from this platform section
|
# Collect bridgeable keys from this platform section
|
||||||
bridged = {}
|
bridged = {}
|
||||||
if "unauthorized_dm_behavior" in platform_cfg:
|
if "unauthorized_dm_behavior" in platform_cfg:
|
||||||
bridged["unauthorized_dm_behavior"] = _normalize_unauthorized_dm_behavior(
|
bridged["unauthorized_dm_behavior"] = (
|
||||||
platform_cfg.get("unauthorized_dm_behavior"),
|
_normalize_unauthorized_dm_behavior(
|
||||||
gw_data.get("unauthorized_dm_behavior", "pair"),
|
platform_cfg.get("unauthorized_dm_behavior"),
|
||||||
|
gw_data.get("unauthorized_dm_behavior", "pair"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if "notice_delivery" in platform_cfg:
|
if "notice_delivery" in platform_cfg:
|
||||||
bridged["notice_delivery"] = _normalize_notice_delivery(
|
bridged["notice_delivery"] = _normalize_notice_delivery(
|
||||||
|
|
@ -1452,7 +1516,9 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
if "reply_in_thread" in platform_cfg:
|
if "reply_in_thread" in platform_cfg:
|
||||||
bridged["reply_in_thread"] = platform_cfg["reply_in_thread"]
|
bridged["reply_in_thread"] = platform_cfg["reply_in_thread"]
|
||||||
if "cron_continuable_surface" in platform_cfg:
|
if "cron_continuable_surface" in platform_cfg:
|
||||||
bridged["cron_continuable_surface"] = platform_cfg["cron_continuable_surface"]
|
bridged["cron_continuable_surface"] = platform_cfg[
|
||||||
|
"cron_continuable_surface"
|
||||||
|
]
|
||||||
if "require_mention" in platform_cfg:
|
if "require_mention" in platform_cfg:
|
||||||
bridged["require_mention"] = platform_cfg["require_mention"]
|
bridged["require_mention"] = platform_cfg["require_mention"]
|
||||||
if plat == Platform.TELEGRAM and "allowed_chats" in platform_cfg:
|
if plat == Platform.TELEGRAM and "allowed_chats" in platform_cfg:
|
||||||
|
|
@ -1462,13 +1528,22 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
if plat == Platform.TELEGRAM and "allowed_topics" in platform_cfg:
|
if plat == Platform.TELEGRAM and "allowed_topics" in platform_cfg:
|
||||||
bridged["allowed_topics"] = platform_cfg["allowed_topics"]
|
bridged["allowed_topics"] = platform_cfg["allowed_topics"]
|
||||||
if "free_response_channels" in platform_cfg:
|
if "free_response_channels" in platform_cfg:
|
||||||
bridged["free_response_channels"] = platform_cfg["free_response_channels"]
|
bridged["free_response_channels"] = platform_cfg[
|
||||||
|
"free_response_channels"
|
||||||
|
]
|
||||||
if "mention_patterns" in platform_cfg:
|
if "mention_patterns" in platform_cfg:
|
||||||
bridged["mention_patterns"] = platform_cfg["mention_patterns"]
|
bridged["mention_patterns"] = platform_cfg["mention_patterns"]
|
||||||
if "exclusive_bot_mentions" in platform_cfg:
|
if "exclusive_bot_mentions" in platform_cfg:
|
||||||
bridged["exclusive_bot_mentions"] = platform_cfg["exclusive_bot_mentions"]
|
bridged["exclusive_bot_mentions"] = platform_cfg[
|
||||||
if plat == Platform.TELEGRAM and "observe_unmentioned_group_messages" in platform_cfg:
|
"exclusive_bot_mentions"
|
||||||
bridged["observe_unmentioned_group_messages"] = platform_cfg["observe_unmentioned_group_messages"]
|
]
|
||||||
|
if (
|
||||||
|
plat == Platform.TELEGRAM
|
||||||
|
and "observe_unmentioned_group_messages" in platform_cfg
|
||||||
|
):
|
||||||
|
bridged["observe_unmentioned_group_messages"] = platform_cfg[
|
||||||
|
"observe_unmentioned_group_messages"
|
||||||
|
]
|
||||||
if "dm_policy" in platform_cfg:
|
if "dm_policy" in platform_cfg:
|
||||||
bridged["dm_policy"] = platform_cfg["dm_policy"]
|
bridged["dm_policy"] = platform_cfg["dm_policy"]
|
||||||
if "allow_from" in platform_cfg:
|
if "allow_from" in platform_cfg:
|
||||||
|
|
@ -1476,25 +1551,40 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
if "allow_admin_from" in platform_cfg:
|
if "allow_admin_from" in platform_cfg:
|
||||||
bridged["allow_admin_from"] = platform_cfg["allow_admin_from"]
|
bridged["allow_admin_from"] = platform_cfg["allow_admin_from"]
|
||||||
if "user_allowed_commands" in platform_cfg:
|
if "user_allowed_commands" in platform_cfg:
|
||||||
bridged["user_allowed_commands"] = platform_cfg["user_allowed_commands"]
|
bridged["user_allowed_commands"] = platform_cfg[
|
||||||
|
"user_allowed_commands"
|
||||||
|
]
|
||||||
if "group_policy" in platform_cfg:
|
if "group_policy" in platform_cfg:
|
||||||
bridged["group_policy"] = platform_cfg["group_policy"]
|
bridged["group_policy"] = platform_cfg["group_policy"]
|
||||||
if "group_allow_from" in platform_cfg:
|
if "group_allow_from" in platform_cfg:
|
||||||
bridged["group_allow_from"] = platform_cfg["group_allow_from"]
|
bridged["group_allow_from"] = platform_cfg["group_allow_from"]
|
||||||
if "group_allow_admin_from" in platform_cfg:
|
if "group_allow_admin_from" in platform_cfg:
|
||||||
bridged["group_allow_admin_from"] = platform_cfg["group_allow_admin_from"]
|
bridged["group_allow_admin_from"] = platform_cfg[
|
||||||
|
"group_allow_admin_from"
|
||||||
|
]
|
||||||
if "group_user_allowed_commands" in platform_cfg:
|
if "group_user_allowed_commands" in platform_cfg:
|
||||||
bridged["group_user_allowed_commands"] = platform_cfg["group_user_allowed_commands"]
|
bridged["group_user_allowed_commands"] = platform_cfg[
|
||||||
if plat in {Platform.DISCORD, Platform.SLACK} and "channel_skill_bindings" in platform_cfg:
|
"group_user_allowed_commands"
|
||||||
bridged["channel_skill_bindings"] = platform_cfg["channel_skill_bindings"]
|
]
|
||||||
|
if (
|
||||||
|
plat in {Platform.DISCORD, Platform.SLACK}
|
||||||
|
and "channel_skill_bindings" in platform_cfg
|
||||||
|
):
|
||||||
|
bridged["channel_skill_bindings"] = platform_cfg[
|
||||||
|
"channel_skill_bindings"
|
||||||
|
]
|
||||||
if "channel_prompts" in platform_cfg:
|
if "channel_prompts" in platform_cfg:
|
||||||
channel_prompts = platform_cfg["channel_prompts"]
|
channel_prompts = platform_cfg["channel_prompts"]
|
||||||
if isinstance(channel_prompts, dict):
|
if isinstance(channel_prompts, dict):
|
||||||
bridged["channel_prompts"] = {str(k): v for k, v in channel_prompts.items()}
|
bridged["channel_prompts"] = {
|
||||||
|
str(k): v for k, v in channel_prompts.items()
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
bridged["channel_prompts"] = channel_prompts
|
bridged["channel_prompts"] = channel_prompts
|
||||||
if "gateway_restart_notification" in platform_cfg:
|
if "gateway_restart_notification" in platform_cfg:
|
||||||
bridged["gateway_restart_notification"] = platform_cfg["gateway_restart_notification"]
|
bridged["gateway_restart_notification"] = platform_cfg[
|
||||||
|
"gateway_restart_notification"
|
||||||
|
]
|
||||||
if "typing_indicator" in platform_cfg:
|
if "typing_indicator" in platform_cfg:
|
||||||
bridged["typing_indicator"] = platform_cfg["typing_indicator"]
|
bridged["typing_indicator"] = platform_cfg["typing_indicator"]
|
||||||
if "typing_status_text" in platform_cfg:
|
if "typing_status_text" in platform_cfg:
|
||||||
|
|
@ -1512,9 +1602,15 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
if isinstance(ov_data, dict)
|
if isinstance(ov_data, dict)
|
||||||
}
|
}
|
||||||
enabled_was_explicit = _cfg_toplevel and "enabled" in platform_cfg
|
enabled_was_explicit = _cfg_toplevel and "enabled" in platform_cfg
|
||||||
if not bridged and not enabled_was_explicit and not has_channel_overrides:
|
if (
|
||||||
|
not bridged
|
||||||
|
and not enabled_was_explicit
|
||||||
|
and not has_channel_overrides
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
plat_data, extra = _ensure_platform_extra_dict(platforms_data, plat.value)
|
plat_data, extra = _ensure_platform_extra_dict(
|
||||||
|
platforms_data, plat.value
|
||||||
|
)
|
||||||
if enabled_was_explicit:
|
if enabled_was_explicit:
|
||||||
plat_data["enabled"] = platform_cfg["enabled"]
|
plat_data["enabled"] = platform_cfg["enabled"]
|
||||||
# Mark the explicit enable/disable so the registry-driven
|
# Mark the explicit enable/disable so the registry-driven
|
||||||
|
|
@ -1554,7 +1650,8 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"apply_yaml_config_fn for %s raised: %s",
|
"apply_yaml_config_fn for %s raised: %s",
|
||||||
entry.name, e,
|
entry.name,
|
||||||
|
e,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if not isinstance(seeded, dict) or not seeded:
|
if not isinstance(seeded, dict) or not seeded:
|
||||||
|
|
@ -1585,7 +1682,9 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
# apply_yaml_config_fn hook — which only runs when a telegram config
|
# apply_yaml_config_fn hook — which only runs when a telegram config
|
||||||
# block exists — can't cover the no-telegram-block case (#3979).
|
# block exists — can't cover the no-telegram-block case (#3979).
|
||||||
if not os.getenv("TELEGRAM_REQUIRE_MENTION"):
|
if not os.getenv("TELEGRAM_REQUIRE_MENTION"):
|
||||||
os.environ["TELEGRAM_REQUIRE_MENTION"] = str(_tl_require_mention).lower()
|
os.environ["TELEGRAM_REQUIRE_MENTION"] = str(
|
||||||
|
_tl_require_mention
|
||||||
|
).lower()
|
||||||
|
|
||||||
# Telegram settings → env vars / extra: migrated to the telegram
|
# Telegram settings → env vars / extra: migrated to the telegram
|
||||||
# plugin's apply_yaml_config_fn hook
|
# plugin's apply_yaml_config_fn hook
|
||||||
|
|
@ -1598,8 +1697,12 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
# Signal settings → env vars (env vars take precedence)
|
# Signal settings → env vars (env vars take precedence)
|
||||||
signal_cfg = yaml_cfg.get("signal", {})
|
signal_cfg = yaml_cfg.get("signal", {})
|
||||||
if isinstance(signal_cfg, dict):
|
if isinstance(signal_cfg, dict):
|
||||||
if "require_mention" in signal_cfg and not os.getenv("SIGNAL_REQUIRE_MENTION"):
|
if "require_mention" in signal_cfg and not os.getenv(
|
||||||
os.environ["SIGNAL_REQUIRE_MENTION"] = str(signal_cfg["require_mention"]).lower()
|
"SIGNAL_REQUIRE_MENTION"
|
||||||
|
):
|
||||||
|
os.environ["SIGNAL_REQUIRE_MENTION"] = str(
|
||||||
|
signal_cfg["require_mention"]
|
||||||
|
).lower()
|
||||||
|
|
||||||
# DingTalk settings → env vars: migrated to the dingtalk plugin's
|
# DingTalk settings → env vars: migrated to the dingtalk plugin's
|
||||||
# apply_yaml_config_fn hook (plugins/platforms/dingtalk/adapter.py).
|
# apply_yaml_config_fn hook (plugins/platforms/dingtalk/adapter.py).
|
||||||
|
|
@ -1628,7 +1731,7 @@ def load_gateway_config() -> GatewayConfig:
|
||||||
|
|
||||||
# Override with environment variables
|
# Override with environment variables
|
||||||
_apply_env_overrides(config)
|
_apply_env_overrides(config)
|
||||||
|
|
||||||
# --- Validate loaded values ---
|
# --- Validate loaded values ---
|
||||||
_validate_gateway_config(config)
|
_validate_gateway_config(config)
|
||||||
|
|
||||||
|
|
@ -1667,7 +1770,8 @@ def _validate_gateway_config(config: "GatewayConfig") -> None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"%s is enabled but %s is empty. "
|
"%s is enabled but %s is empty. "
|
||||||
"The adapter will likely fail to connect.",
|
"The adapter will likely fail to connect.",
|
||||||
platform.value, env_name,
|
platform.value,
|
||||||
|
env_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reject known-weak placeholder tokens.
|
# Reject known-weak placeholder tokens.
|
||||||
|
|
@ -1692,7 +1796,9 @@ def _validate_gateway_config(config: "GatewayConfig") -> None:
|
||||||
"%s is enabled but %s is set to a placeholder value ('%s'). "
|
"%s is enabled but %s is set to a placeholder value ('%s'). "
|
||||||
"Set a real bot token before starting the gateway. "
|
"Set a real bot token before starting the gateway. "
|
||||||
"The adapter will NOT be started.",
|
"The adapter will NOT be started.",
|
||||||
platform.value, env_name, token.strip()[:6] + "...",
|
platform.value,
|
||||||
|
env_name,
|
||||||
|
token.strip()[:6] + "...",
|
||||||
)
|
)
|
||||||
pconfig.enabled = False
|
pconfig.enabled = False
|
||||||
|
|
||||||
|
|
@ -1714,24 +1820,26 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
# platforms — telegram, matrix — flow through here too, #41112). The
|
# platforms — telegram, matrix — flow through here too, #41112). The
|
||||||
# flag is cleared once for all platforms in the final cleanup at the
|
# flag is cleared once for all platforms in the final cleanup at the
|
||||||
# end of _apply_env_overrides.
|
# end of _apply_env_overrides.
|
||||||
enabled_was_explicit = bool(platform_config.extra.get("_enabled_explicit", False))
|
enabled_was_explicit = bool(
|
||||||
|
platform_config.extra.get("_enabled_explicit", False)
|
||||||
|
)
|
||||||
if not platform_config.enabled and not enabled_was_explicit:
|
if not platform_config.enabled and not enabled_was_explicit:
|
||||||
platform_config.enabled = True
|
platform_config.enabled = True
|
||||||
return platform_config
|
return platform_config
|
||||||
|
|
||||||
# Telegram
|
# Telegram
|
||||||
telegram_token = getenv("TELEGRAM_BOT_TOKEN")
|
telegram_token = getenv("TELEGRAM_BOT_TOKEN")
|
||||||
if telegram_token:
|
if telegram_token:
|
||||||
telegram_config = _enable_from_env(Platform.TELEGRAM)
|
telegram_config = _enable_from_env(Platform.TELEGRAM)
|
||||||
telegram_config.token = telegram_token
|
telegram_config.token = telegram_token
|
||||||
|
|
||||||
# Reply threading mode for Telegram (off/first/all)
|
# Reply threading mode for Telegram (off/first/all)
|
||||||
telegram_reply_mode = getenv("TELEGRAM_REPLY_TO_MODE", "").lower()
|
telegram_reply_mode = getenv("TELEGRAM_REPLY_TO_MODE", "").lower()
|
||||||
if telegram_reply_mode in {"off", "first", "all"}:
|
if telegram_reply_mode in {"off", "first", "all"}:
|
||||||
if Platform.TELEGRAM not in config.platforms:
|
if Platform.TELEGRAM not in config.platforms:
|
||||||
config.platforms[Platform.TELEGRAM] = PlatformConfig()
|
config.platforms[Platform.TELEGRAM] = PlatformConfig()
|
||||||
config.platforms[Platform.TELEGRAM].reply_to_mode = telegram_reply_mode
|
config.platforms[Platform.TELEGRAM].reply_to_mode = telegram_reply_mode
|
||||||
|
|
||||||
telegram_fallback_ips = getenv("TELEGRAM_FALLBACK_IPS", "")
|
telegram_fallback_ips = getenv("TELEGRAM_FALLBACK_IPS", "")
|
||||||
if telegram_fallback_ips:
|
if telegram_fallback_ips:
|
||||||
if Platform.TELEGRAM not in config.platforms:
|
if Platform.TELEGRAM not in config.platforms:
|
||||||
|
|
@ -1748,13 +1856,13 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
name=getenv("TELEGRAM_HOME_CHANNEL_NAME", "Home"),
|
name=getenv("TELEGRAM_HOME_CHANNEL_NAME", "Home"),
|
||||||
thread_id=getenv("TELEGRAM_HOME_CHANNEL_THREAD_ID") or None,
|
thread_id=getenv("TELEGRAM_HOME_CHANNEL_THREAD_ID") or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Discord
|
# Discord
|
||||||
discord_token = getenv("DISCORD_BOT_TOKEN")
|
discord_token = getenv("DISCORD_BOT_TOKEN")
|
||||||
if discord_token:
|
if discord_token:
|
||||||
discord_config = _enable_from_env(Platform.DISCORD)
|
discord_config = _enable_from_env(Platform.DISCORD)
|
||||||
discord_config.token = discord_token
|
discord_config.token = discord_token
|
||||||
|
|
||||||
discord_home = getenv("DISCORD_HOME_CHANNEL")
|
discord_home = getenv("DISCORD_HOME_CHANNEL")
|
||||||
if discord_home and Platform.DISCORD in config.platforms:
|
if discord_home and Platform.DISCORD in config.platforms:
|
||||||
config.platforms[Platform.DISCORD].home_channel = HomeChannel(
|
config.platforms[Platform.DISCORD].home_channel = HomeChannel(
|
||||||
|
|
@ -1763,17 +1871,21 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
name=getenv("DISCORD_HOME_CHANNEL_NAME", "Home"),
|
name=getenv("DISCORD_HOME_CHANNEL_NAME", "Home"),
|
||||||
thread_id=getenv("DISCORD_HOME_CHANNEL_THREAD_ID") or None,
|
thread_id=getenv("DISCORD_HOME_CHANNEL_THREAD_ID") or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reply threading mode for Discord (off/first/all)
|
# Reply threading mode for Discord (off/first/all)
|
||||||
discord_reply_mode = getenv("DISCORD_REPLY_TO_MODE", "").lower()
|
discord_reply_mode = getenv("DISCORD_REPLY_TO_MODE", "").lower()
|
||||||
if discord_reply_mode in {"off", "first", "all"}:
|
if discord_reply_mode in {"off", "first", "all"}:
|
||||||
if Platform.DISCORD not in config.platforms:
|
if Platform.DISCORD not in config.platforms:
|
||||||
config.platforms[Platform.DISCORD] = PlatformConfig()
|
config.platforms[Platform.DISCORD] = PlatformConfig()
|
||||||
config.platforms[Platform.DISCORD].reply_to_mode = discord_reply_mode
|
config.platforms[Platform.DISCORD].reply_to_mode = discord_reply_mode
|
||||||
|
|
||||||
# WhatsApp (typically uses different auth mechanism)
|
# WhatsApp (typically uses different auth mechanism)
|
||||||
whatsapp_enabled = is_truthy_value(getenv("WHATSAPP_ENABLED", ""))
|
whatsapp_enabled = is_truthy_value(getenv("WHATSAPP_ENABLED", ""))
|
||||||
whatsapp_disabled_explicitly = getenv("WHATSAPP_ENABLED", "").lower() in {"false", "0", "no"}
|
whatsapp_disabled_explicitly = getenv("WHATSAPP_ENABLED", "").lower() in {
|
||||||
|
"false",
|
||||||
|
"0",
|
||||||
|
"no",
|
||||||
|
}
|
||||||
if Platform.WHATSAPP in config.platforms:
|
if Platform.WHATSAPP in config.platforms:
|
||||||
# YAML config exists — respect explicit disable
|
# YAML config exists — respect explicit disable
|
||||||
wa_cfg = config.platforms[Platform.WHATSAPP]
|
wa_cfg = config.platforms[Platform.WHATSAPP]
|
||||||
|
|
@ -1813,32 +1925,46 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["app_id"] = wa_cloud_app_id
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["app_id"] = wa_cloud_app_id
|
||||||
wa_cloud_app_secret = getenv("WHATSAPP_CLOUD_APP_SECRET")
|
wa_cloud_app_secret = getenv("WHATSAPP_CLOUD_APP_SECRET")
|
||||||
if wa_cloud_app_secret:
|
if wa_cloud_app_secret:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["app_secret"] = wa_cloud_app_secret
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["app_secret"] = (
|
||||||
|
wa_cloud_app_secret
|
||||||
|
)
|
||||||
# Optional: WABA id (analytics, future use)
|
# Optional: WABA id (analytics, future use)
|
||||||
wa_cloud_waba_id = getenv("WHATSAPP_CLOUD_WABA_ID")
|
wa_cloud_waba_id = getenv("WHATSAPP_CLOUD_WABA_ID")
|
||||||
if wa_cloud_waba_id:
|
if wa_cloud_waba_id:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["waba_id"] = wa_cloud_waba_id
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["waba_id"] = (
|
||||||
|
wa_cloud_waba_id
|
||||||
|
)
|
||||||
# Webhook verify token — Meta hub.verify_token shared secret
|
# Webhook verify token — Meta hub.verify_token shared secret
|
||||||
wa_cloud_verify_token = getenv("WHATSAPP_CLOUD_VERIFY_TOKEN")
|
wa_cloud_verify_token = getenv("WHATSAPP_CLOUD_VERIFY_TOKEN")
|
||||||
if wa_cloud_verify_token:
|
if wa_cloud_verify_token:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["verify_token"] = wa_cloud_verify_token
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["verify_token"] = (
|
||||||
|
wa_cloud_verify_token
|
||||||
|
)
|
||||||
# Webhook server bind config (defaults baked into the adapter)
|
# Webhook server bind config (defaults baked into the adapter)
|
||||||
wa_cloud_host = getenv("WHATSAPP_CLOUD_WEBHOOK_HOST")
|
wa_cloud_host = getenv("WHATSAPP_CLOUD_WEBHOOK_HOST")
|
||||||
if wa_cloud_host:
|
if wa_cloud_host:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["webhook_host"] = wa_cloud_host
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["webhook_host"] = (
|
||||||
|
wa_cloud_host
|
||||||
|
)
|
||||||
wa_cloud_port = getenv("WHATSAPP_CLOUD_WEBHOOK_PORT")
|
wa_cloud_port = getenv("WHATSAPP_CLOUD_WEBHOOK_PORT")
|
||||||
if wa_cloud_port:
|
if wa_cloud_port:
|
||||||
try:
|
try:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["webhook_port"] = int(wa_cloud_port)
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["webhook_port"] = int(
|
||||||
|
wa_cloud_port
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
wa_cloud_path = getenv("WHATSAPP_CLOUD_WEBHOOK_PATH")
|
wa_cloud_path = getenv("WHATSAPP_CLOUD_WEBHOOK_PATH")
|
||||||
if wa_cloud_path:
|
if wa_cloud_path:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["webhook_path"] = wa_cloud_path
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["webhook_path"] = (
|
||||||
|
wa_cloud_path
|
||||||
|
)
|
||||||
# Graph API version override (rarely needed)
|
# Graph API version override (rarely needed)
|
||||||
wa_cloud_api_version = getenv("WHATSAPP_CLOUD_API_VERSION")
|
wa_cloud_api_version = getenv("WHATSAPP_CLOUD_API_VERSION")
|
||||||
if wa_cloud_api_version:
|
if wa_cloud_api_version:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].extra["api_version"] = wa_cloud_api_version
|
config.platforms[Platform.WHATSAPP_CLOUD].extra["api_version"] = (
|
||||||
|
wa_cloud_api_version
|
||||||
|
)
|
||||||
whatsapp_cloud_home = getenv("WHATSAPP_CLOUD_HOME_CHANNEL")
|
whatsapp_cloud_home = getenv("WHATSAPP_CLOUD_HOME_CHANNEL")
|
||||||
if whatsapp_cloud_home and Platform.WHATSAPP_CLOUD in config.platforms:
|
if whatsapp_cloud_home and Platform.WHATSAPP_CLOUD in config.platforms:
|
||||||
config.platforms[Platform.WHATSAPP_CLOUD].home_channel = HomeChannel(
|
config.platforms[Platform.WHATSAPP_CLOUD].home_channel = HomeChannel(
|
||||||
|
|
@ -1862,7 +1988,9 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
# platform the user explicitly disabled (Slack is now a plugin
|
# platform the user explicitly disabled (Slack is now a plugin
|
||||||
# entry — #41112). The flag is cleared once for all platforms in
|
# entry — #41112). The flag is cleared once for all platforms in
|
||||||
# the final cleanup at the end of _apply_env_overrides.
|
# the final cleanup at the end of _apply_env_overrides.
|
||||||
enabled_was_explicit = bool(slack_config.extra.get("_enabled_explicit", False))
|
enabled_was_explicit = bool(
|
||||||
|
slack_config.extra.get("_enabled_explicit", False)
|
||||||
|
)
|
||||||
if not slack_config.enabled and not enabled_was_explicit:
|
if not slack_config.enabled and not enabled_was_explicit:
|
||||||
# Top-level Slack settings such as channel prompts should not
|
# Top-level Slack settings such as channel prompts should not
|
||||||
# turn an env-token setup into a disabled platform. Only an
|
# turn an env-token setup into a disabled platform. Only an
|
||||||
|
|
@ -1880,7 +2008,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
name=getenv("SLACK_HOME_CHANNEL_NAME", ""),
|
name=getenv("SLACK_HOME_CHANNEL_NAME", ""),
|
||||||
thread_id=getenv("SLACK_HOME_CHANNEL_THREAD_ID") or None,
|
thread_id=getenv("SLACK_HOME_CHANNEL_THREAD_ID") or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Signal
|
# Signal
|
||||||
signal_url = getenv("SIGNAL_HTTP_URL")
|
signal_url = getenv("SIGNAL_HTTP_URL")
|
||||||
signal_account = getenv("SIGNAL_ACCOUNT")
|
signal_account = getenv("SIGNAL_ACCOUNT")
|
||||||
|
|
@ -1923,7 +2051,9 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
matrix_homeserver = getenv("MATRIX_HOMESERVER", "")
|
matrix_homeserver = getenv("MATRIX_HOMESERVER", "")
|
||||||
if matrix_token or getenv("MATRIX_PASSWORD"):
|
if matrix_token or getenv("MATRIX_PASSWORD"):
|
||||||
if not matrix_homeserver:
|
if not matrix_homeserver:
|
||||||
logger.warning("MATRIX_ACCESS_TOKEN/MATRIX_PASSWORD set but MATRIX_HOMESERVER is missing")
|
logger.warning(
|
||||||
|
"MATRIX_ACCESS_TOKEN/MATRIX_PASSWORD set but MATRIX_HOMESERVER is missing"
|
||||||
|
)
|
||||||
matrix_config = _enable_from_env(Platform.MATRIX)
|
matrix_config = _enable_from_env(Platform.MATRIX)
|
||||||
if matrix_token:
|
if matrix_token:
|
||||||
matrix_config.token = matrix_token
|
matrix_config.token = matrix_token
|
||||||
|
|
@ -1935,10 +2065,13 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
if matrix_password:
|
if matrix_password:
|
||||||
matrix_config.extra["password"] = matrix_password
|
matrix_config.extra["password"] = matrix_password
|
||||||
matrix_e2ee_mode = getenv("MATRIX_E2EE_MODE", "").strip().lower()
|
matrix_e2ee_mode = getenv("MATRIX_E2EE_MODE", "").strip().lower()
|
||||||
matrix_e2ee = (
|
matrix_e2ee = matrix_e2ee_mode in (
|
||||||
matrix_e2ee_mode in ("required", "require", "optional", "prefer", "preferred")
|
"required",
|
||||||
or is_truthy_value(getenv("MATRIX_ENCRYPTION", ""))
|
"require",
|
||||||
)
|
"optional",
|
||||||
|
"prefer",
|
||||||
|
"preferred",
|
||||||
|
) or is_truthy_value(getenv("MATRIX_ENCRYPTION", ""))
|
||||||
matrix_config.extra["encryption"] = matrix_e2ee
|
matrix_config.extra["encryption"] = matrix_e2ee
|
||||||
if matrix_e2ee_mode:
|
if matrix_e2ee_mode:
|
||||||
matrix_config.extra["e2ee_mode"] = matrix_e2ee_mode
|
matrix_config.extra["e2ee_mode"] = matrix_e2ee_mode
|
||||||
|
|
@ -2020,19 +2153,27 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
if api_server_key:
|
if api_server_key:
|
||||||
config.platforms[Platform.API_SERVER].extra["key"] = api_server_key
|
config.platforms[Platform.API_SERVER].extra["key"] = api_server_key
|
||||||
if api_server_cors_origins:
|
if api_server_cors_origins:
|
||||||
origins = [origin.strip() for origin in api_server_cors_origins.split(",") if origin.strip()]
|
origins = [
|
||||||
|
origin.strip()
|
||||||
|
for origin in api_server_cors_origins.split(",")
|
||||||
|
if origin.strip()
|
||||||
|
]
|
||||||
if origins:
|
if origins:
|
||||||
config.platforms[Platform.API_SERVER].extra["cors_origins"] = origins
|
config.platforms[Platform.API_SERVER].extra["cors_origins"] = origins
|
||||||
if api_server_port:
|
if api_server_port:
|
||||||
try:
|
try:
|
||||||
config.platforms[Platform.API_SERVER].extra["port"] = int(api_server_port)
|
config.platforms[Platform.API_SERVER].extra["port"] = int(
|
||||||
|
api_server_port
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if api_server_host:
|
if api_server_host:
|
||||||
config.platforms[Platform.API_SERVER].extra["host"] = api_server_host
|
config.platforms[Platform.API_SERVER].extra["host"] = api_server_host
|
||||||
api_server_model_name = getenv("API_SERVER_MODEL_NAME", "")
|
api_server_model_name = getenv("API_SERVER_MODEL_NAME", "")
|
||||||
if api_server_model_name:
|
if api_server_model_name:
|
||||||
config.platforms[Platform.API_SERVER].extra["model_name"] = api_server_model_name
|
config.platforms[Platform.API_SERVER].extra["model_name"] = (
|
||||||
|
api_server_model_name
|
||||||
|
)
|
||||||
|
|
||||||
# Webhook platform
|
# Webhook platform
|
||||||
webhook_enabled = is_truthy_value(getenv("WEBHOOK_ENABLED", ""))
|
webhook_enabled = is_truthy_value(getenv("WEBHOOK_ENABLED", ""))
|
||||||
|
|
@ -2055,9 +2196,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
msgraph_webhook_port = getenv("MSGRAPH_WEBHOOK_PORT")
|
msgraph_webhook_port = getenv("MSGRAPH_WEBHOOK_PORT")
|
||||||
msgraph_webhook_client_state = getenv("MSGRAPH_WEBHOOK_CLIENT_STATE", "")
|
msgraph_webhook_client_state = getenv("MSGRAPH_WEBHOOK_CLIENT_STATE", "")
|
||||||
msgraph_webhook_resources = getenv("MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES", "")
|
msgraph_webhook_resources = getenv("MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES", "")
|
||||||
msgraph_webhook_allowed_cidrs = getenv(
|
msgraph_webhook_allowed_cidrs = getenv("MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS", "")
|
||||||
"MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS", ""
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
msgraph_webhook_enabled
|
msgraph_webhook_enabled
|
||||||
or Platform.MSGRAPH_WEBHOOK in config.platforms
|
or Platform.MSGRAPH_WEBHOOK in config.platforms
|
||||||
|
|
@ -2140,7 +2279,9 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
config.platforms[Platform.FEISHU].extra["encrypt_key"] = feishu_encrypt_key
|
config.platforms[Platform.FEISHU].extra["encrypt_key"] = feishu_encrypt_key
|
||||||
feishu_verification_token = getenv("FEISHU_VERIFICATION_TOKEN", "")
|
feishu_verification_token = getenv("FEISHU_VERIFICATION_TOKEN", "")
|
||||||
if feishu_verification_token:
|
if feishu_verification_token:
|
||||||
config.platforms[Platform.FEISHU].extra["verification_token"] = feishu_verification_token
|
config.platforms[Platform.FEISHU].extra["verification_token"] = (
|
||||||
|
feishu_verification_token
|
||||||
|
)
|
||||||
feishu_home = getenv("FEISHU_HOME_CHANNEL")
|
feishu_home = getenv("FEISHU_HOME_CHANNEL")
|
||||||
if feishu_home:
|
if feishu_home:
|
||||||
config.platforms[Platform.FEISHU].home_channel = HomeChannel(
|
config.platforms[Platform.FEISHU].home_channel = HomeChannel(
|
||||||
|
|
@ -2245,7 +2386,9 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
"webhook_host": getenv("BLUEBUBBLES_WEBHOOK_HOST", "127.0.0.1"),
|
"webhook_host": getenv("BLUEBUBBLES_WEBHOOK_HOST", "127.0.0.1"),
|
||||||
"webhook_port": getenv_int("BLUEBUBBLES_WEBHOOK_PORT", 8645),
|
"webhook_port": getenv_int("BLUEBUBBLES_WEBHOOK_PORT", 8645),
|
||||||
"webhook_path": getenv("BLUEBUBBLES_WEBHOOK_PATH", "/bluebubbles-webhook"),
|
"webhook_path": getenv("BLUEBUBBLES_WEBHOOK_PATH", "/bluebubbles-webhook"),
|
||||||
"send_read_receipts": is_truthy_value(getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true")),
|
"send_read_receipts": is_truthy_value(
|
||||||
|
getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true")
|
||||||
|
),
|
||||||
})
|
})
|
||||||
bluebubbles_require_mention = getenv("BLUEBUBBLES_REQUIRE_MENTION")
|
bluebubbles_require_mention = getenv("BLUEBUBBLES_REQUIRE_MENTION")
|
||||||
if bluebubbles_require_mention is not None:
|
if bluebubbles_require_mention is not None:
|
||||||
|
|
@ -2259,10 +2402,14 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
except Exception:
|
except Exception:
|
||||||
parsed_patterns = [
|
parsed_patterns = [
|
||||||
part.strip()
|
part.strip()
|
||||||
for part in bluebubbles_mention_patterns.replace("\n", ",").split(",")
|
for part in bluebubbles_mention_patterns.replace("\n", ",").split(
|
||||||
|
","
|
||||||
|
)
|
||||||
if part.strip()
|
if part.strip()
|
||||||
]
|
]
|
||||||
config.platforms[Platform.BLUEBUBBLES].extra["mention_patterns"] = parsed_patterns
|
config.platforms[Platform.BLUEBUBBLES].extra["mention_patterns"] = (
|
||||||
|
parsed_patterns
|
||||||
|
)
|
||||||
bluebubbles_home = getenv("BLUEBUBBLES_HOME_CHANNEL")
|
bluebubbles_home = getenv("BLUEBUBBLES_HOME_CHANNEL")
|
||||||
if bluebubbles_home and Platform.BLUEBUBBLES in config.platforms:
|
if bluebubbles_home and Platform.BLUEBUBBLES in config.platforms:
|
||||||
config.platforms[Platform.BLUEBUBBLES].home_channel = HomeChannel(
|
config.platforms[Platform.BLUEBUBBLES].home_channel = HomeChannel(
|
||||||
|
|
@ -2306,7 +2453,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
config.platforms[Platform.QQBOT].home_channel = HomeChannel(
|
config.platforms[Platform.QQBOT].home_channel = HomeChannel(
|
||||||
platform=Platform.QQBOT,
|
platform=Platform.QQBOT,
|
||||||
chat_id=qq_home,
|
chat_id=qq_home,
|
||||||
name=getenv("QQBOT_HOME_CHANNEL_NAME") or getenv(qq_home_name_env, "Home"),
|
name=getenv("QQBOT_HOME_CHANNEL_NAME")
|
||||||
|
or getenv(qq_home_name_env, "Home"),
|
||||||
thread_id=(
|
thread_id=(
|
||||||
getenv("QQBOT_HOME_CHANNEL_THREAD_ID")
|
getenv("QQBOT_HOME_CHANNEL_THREAD_ID")
|
||||||
or getenv("QQ_HOME_CHANNEL_THREAD_ID")
|
or getenv("QQ_HOME_CHANNEL_THREAD_ID")
|
||||||
|
|
@ -2364,7 +2512,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
config.default_reset_policy.idle_minutes = int(idle_minutes)
|
config.default_reset_policy.idle_minutes = int(idle_minutes)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
reset_hour = getenv("SESSION_RESET_HOUR")
|
reset_hour = getenv("SESSION_RESET_HOUR")
|
||||||
if reset_hour:
|
if reset_hour:
|
||||||
try:
|
try:
|
||||||
|
|
@ -2392,8 +2540,10 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
# counterpart.
|
# counterpart.
|
||||||
try:
|
try:
|
||||||
from hermes_cli.plugins import discover_plugins
|
from hermes_cli.plugins import discover_plugins
|
||||||
|
|
||||||
discover_plugins() # idempotent
|
discover_plugins() # idempotent
|
||||||
from gateway.platform_registry import platform_registry
|
from gateway.platform_registry import platform_registry
|
||||||
|
|
||||||
for entry in platform_registry.plugin_entries():
|
for entry in platform_registry.plugin_entries():
|
||||||
try:
|
try:
|
||||||
platform = Platform(entry.name)
|
platform = Platform(entry.name)
|
||||||
|
|
@ -2428,9 +2578,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
try:
|
try:
|
||||||
seed_for_probe = entry.env_enablement_fn()
|
seed_for_probe = entry.env_enablement_fn()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(
|
logger.debug("env_enablement_fn for %s raised: %s", entry.name, e)
|
||||||
"env_enablement_fn for %s raised: %s", entry.name, e
|
|
||||||
)
|
|
||||||
seed_for_probe = None
|
seed_for_probe = None
|
||||||
|
|
||||||
# Only consult is_connected for platforms that are NOT already
|
# Only consult is_connected for platforms that are NOT already
|
||||||
|
|
@ -2473,7 +2621,8 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"is_connected for %s raised: %s — skipping enablement",
|
"is_connected for %s raised: %s — skipping enablement",
|
||||||
entry.name, exc,
|
entry.name,
|
||||||
|
exc,
|
||||||
)
|
)
|
||||||
configured = False
|
configured = False
|
||||||
if not configured:
|
if not configured:
|
||||||
|
|
@ -2518,9 +2667,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
chat_id=str(home["chat_id"]),
|
chat_id=str(home["chat_id"]),
|
||||||
name=str(home.get("name") or "Home"),
|
name=str(home.get("name") or "Home"),
|
||||||
thread_id=(
|
thread_id=(
|
||||||
str(home["thread_id"])
|
str(home["thread_id"]) if home.get("thread_id") else None
|
||||||
if home.get("thread_id")
|
|
||||||
else None
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,7 +8,11 @@ from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gateway.config import Platform, _PLATFORM_CONNECTED_CHECKERS, _BUILTIN_PLATFORM_VALUES
|
from gateway.config import (
|
||||||
|
Platform,
|
||||||
|
_PLATFORM_CONNECTED_CHECKERS,
|
||||||
|
_BUILTIN_PLATFORM_VALUES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_all_builtins_have_checker_or_generic_token_path():
|
def test_all_builtins_have_checker_or_generic_token_path():
|
||||||
|
|
@ -21,14 +25,17 @@ def test_all_builtins_have_checker_or_generic_token_path():
|
||||||
a built-in just because nobody added it to the checker dict.
|
a built-in just because nobody added it to the checker dict.
|
||||||
"""
|
"""
|
||||||
# Platforms covered by the generic token/api_key branch
|
# Platforms covered by the generic token/api_key branch
|
||||||
generic_token_values = {p.value for p in {
|
generic_token_values = {
|
||||||
Platform.TELEGRAM,
|
p.value
|
||||||
Platform.DISCORD,
|
for p in {
|
||||||
Platform.SLACK,
|
Platform.TELEGRAM,
|
||||||
Platform.MATRIX,
|
Platform.DISCORD,
|
||||||
Platform.MATTERMOST,
|
Platform.SLACK,
|
||||||
Platform.HOMEASSISTANT,
|
Platform.MATRIX,
|
||||||
}}
|
Platform.MATTERMOST,
|
||||||
|
Platform.HOMEASSISTANT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Platforms with a bespoke checker
|
# Platforms with a bespoke checker
|
||||||
checker_values = {p.value for p in set(_PLATFORM_CONNECTED_CHECKERS.keys())}
|
checker_values = {p.value for p in set(_PLATFORM_CONNECTED_CHECKERS.keys())}
|
||||||
|
|
@ -42,6 +49,7 @@ def test_all_builtins_have_checker_or_generic_token_path():
|
||||||
try:
|
try:
|
||||||
from hermes_cli.plugins import discover_plugins
|
from hermes_cli.plugins import discover_plugins
|
||||||
from gateway.platform_registry import platform_registry
|
from gateway.platform_registry import platform_registry
|
||||||
|
|
||||||
discover_plugins()
|
discover_plugins()
|
||||||
for _entry in platform_registry.all_entries():
|
for _entry in platform_registry.all_entries():
|
||||||
if _entry.is_connected is not None or _entry.validate_config is not None:
|
if _entry.is_connected is not None or _entry.validate_config is not None:
|
||||||
|
|
@ -66,7 +74,9 @@ def test_all_builtins_have_checker_or_generic_token_path():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items()))
|
@pytest.mark.parametrize(
|
||||||
|
"platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items())
|
||||||
|
)
|
||||||
def test_checker_handles_minimal_config(platform, checker):
|
def test_checker_handles_minimal_config(platform, checker):
|
||||||
"""Each bespoke checker must not crash on a minimal PlatformConfig."""
|
"""Each bespoke checker must not crash on a minimal PlatformConfig."""
|
||||||
mock_config = MagicMock()
|
mock_config = MagicMock()
|
||||||
|
|
@ -80,7 +90,9 @@ def test_checker_handles_minimal_config(platform, checker):
|
||||||
assert isinstance(result, bool)
|
assert isinstance(result, bool)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items()))
|
@pytest.mark.parametrize(
|
||||||
|
"platform, checker", list(_PLATFORM_CONNECTED_CHECKERS.items())
|
||||||
|
)
|
||||||
def test_checker_returns_true_when_configured(platform, checker, monkeypatch):
|
def test_checker_returns_true_when_configured(platform, checker, monkeypatch):
|
||||||
"""Each bespoke checker must return True when the config looks valid."""
|
"""Each bespoke checker must return True when the config looks valid."""
|
||||||
mock_config = MagicMock()
|
mock_config = MagicMock()
|
||||||
|
|
@ -99,7 +111,7 @@ def test_checker_returns_true_when_configured(platform, checker, monkeypatch):
|
||||||
monkeypatch.setenv("TWILIO_ACCOUNT_SID", "ACtest")
|
monkeypatch.setenv("TWILIO_ACCOUNT_SID", "ACtest")
|
||||||
mock_config.extra = {}
|
mock_config.extra = {}
|
||||||
elif platform == Platform.API_SERVER:
|
elif platform == Platform.API_SERVER:
|
||||||
mock_config.extra = {"key": "sk-mykey"}
|
mock_config.extra = {"key": "opensslrandhex32strongkey"}
|
||||||
elif platform in {
|
elif platform in {
|
||||||
Platform.WEBHOOK,
|
Platform.WEBHOOK,
|
||||||
Platform.WHATSAPP,
|
Platform.WHATSAPP,
|
||||||
|
|
@ -127,4 +139,28 @@ def test_checker_returns_true_when_configured(platform, checker, monkeypatch):
|
||||||
pytest.skip(f"No synthetic config defined for {platform.value}")
|
pytest.skip(f"No synthetic config defined for {platform.value}")
|
||||||
|
|
||||||
result = checker(mock_config)
|
result = checker(mock_config)
|
||||||
assert result is True, f"{platform.value} checker should return True with valid-looking config"
|
assert result is True, (
|
||||||
|
f"{platform.value} checker should return True with valid-looking config"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_server_checker_key_validity():
|
||||||
|
"""Test Platform.API_SERVER checker with missing, placeholder, short, and strong keys."""
|
||||||
|
checker = _PLATFORM_CONNECTED_CHECKERS[Platform.API_SERVER]
|
||||||
|
|
||||||
|
# Missing
|
||||||
|
cfg = MagicMock()
|
||||||
|
cfg.extra = {}
|
||||||
|
assert checker(cfg) is False
|
||||||
|
|
||||||
|
# Placeholder
|
||||||
|
cfg.extra = {"key": "dummy"}
|
||||||
|
assert checker(cfg) is False
|
||||||
|
|
||||||
|
# Too short
|
||||||
|
cfg.extra = {"key": "shortkey"}
|
||||||
|
assert checker(cfg) is False
|
||||||
|
|
||||||
|
# Strong key (>=16 chars)
|
||||||
|
cfg.extra = {"key": "opensslrandhex32strongkey"}
|
||||||
|
assert checker(cfg) is True
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue