mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
Address email pairing review feedback
This commit is contained in:
parent
2455e1801b
commit
5dae502b86
6 changed files with 71 additions and 35 deletions
|
|
@ -457,17 +457,19 @@ class GatewayAuthorizationMixin:
|
|||
|
||||
Resolution order:
|
||||
1. Explicit per-platform ``unauthorized_dm_behavior`` in config — always wins.
|
||||
2. Explicit global ``unauthorized_dm_behavior`` in config — wins when no per-platform.
|
||||
3. Email defaults to ``"ignore"`` unless explicitly opted into
|
||||
2. Email defaults to ``"ignore"`` unless explicitly opted into
|
||||
pairing. Inboxes may contain arbitrary unread human messages, so
|
||||
replying with pairing codes is not a safe platform default.
|
||||
4. When an allowlist (``PLATFORM_ALLOWED_USERS``,
|
||||
3. Explicit global ``unauthorized_dm_behavior`` in config — wins for
|
||||
chat-shaped platforms when no per-platform override is set.
|
||||
4. When an adapter-level DM policy opts into pairing or silent drop, honor it.
|
||||
5. When an allowlist (``PLATFORM_ALLOWED_USERS``,
|
||||
``PLATFORM_GROUP_ALLOWED_USERS`` / ``PLATFORM_GROUP_ALLOWED_CHATS``,
|
||||
or ``GATEWAY_ALLOWED_USERS``) is configured, default to ``"ignore"`` —
|
||||
the allowlist signals that the owner has deliberately restricted
|
||||
access; spamming unknown contacts with pairing codes is both noisy
|
||||
and a potential info-leak. (#9337)
|
||||
5. No allowlist and no explicit config → ``"pair"`` (open-gateway default).
|
||||
6. No allowlist and no explicit config → ``"pair"`` (open-gateway default).
|
||||
"""
|
||||
config = getattr(self, "config", None)
|
||||
|
||||
|
|
@ -478,6 +480,14 @@ class GatewayAuthorizationMixin:
|
|||
# Operator explicitly configured behavior for this platform — respect it.
|
||||
return config.get_unauthorized_dm_behavior(platform)
|
||||
|
||||
# Email is inbox-shaped, not chat-shaped: an agent mailbox may contain
|
||||
# unrelated unread human email. Require an explicit per-platform
|
||||
# ``unauthorized_dm_behavior: pair`` opt-in before replying to unknown
|
||||
# senders with pairing codes. Keep this before the global fallback to
|
||||
# match GatewayConfig.get_unauthorized_dm_behavior().
|
||||
if platform == Platform.EMAIL:
|
||||
return "ignore"
|
||||
|
||||
# Check for an explicit global config override.
|
||||
if config and hasattr(config, "unauthorized_dm_behavior"):
|
||||
if config.unauthorized_dm_behavior != "pair": # non-default → explicit override
|
||||
|
|
@ -497,13 +507,6 @@ class GatewayAuthorizationMixin:
|
|||
if dm_policy in {"allowlist", "disabled"}:
|
||||
return "ignore"
|
||||
|
||||
# Email is inbox-shaped, not chat-shaped: an agent mailbox may contain
|
||||
# unrelated unread human email. Require an explicit per-platform
|
||||
# ``unauthorized_dm_behavior: pair`` opt-in before replying to unknown
|
||||
# senders with pairing codes.
|
||||
if platform == Platform.EMAIL:
|
||||
return "ignore"
|
||||
|
||||
# No explicit override. Fall back to allowlist-aware default:
|
||||
# if any allowlist is configured for this platform, silently drop
|
||||
# unauthorized messages instead of sending pairing codes.
|
||||
|
|
|
|||
|
|
@ -749,7 +749,12 @@ class GatewayConfig:
|
|||
)
|
||||
|
||||
def get_unauthorized_dm_behavior(self, platform: Optional[Platform] = None) -> str:
|
||||
"""Return the effective unauthorized-DM behavior for a platform."""
|
||||
"""Return the effective unauthorized-DM behavior for a platform.
|
||||
|
||||
Email is inbox-shaped, not chat-shaped, so it defaults to ``"ignore"``
|
||||
unless ``platforms.email.unauthorized_dm_behavior`` explicitly opts
|
||||
into pairing. A global default does not opt email into pairing.
|
||||
"""
|
||||
if platform:
|
||||
platform_cfg = self.platforms.get(platform)
|
||||
if platform_cfg and "unauthorized_dm_behavior" in platform_cfg.extra:
|
||||
|
|
|
|||
|
|
@ -5636,6 +5636,34 @@ def load_config_readonly() -> Dict[str, Any]:
|
|||
return _load_config_impl(want_deepcopy=False)
|
||||
|
||||
|
||||
def write_platform_config_field(
|
||||
platform_key: str,
|
||||
field_key: str,
|
||||
value: Any,
|
||||
*,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
"""Persist one scalar field under ``platforms.<platform_key>``.
|
||||
|
||||
``raw=True`` preserves CLI setup flows that intentionally edit only the
|
||||
user's raw config file. Dashboard routes use the default loaded-config path
|
||||
so they retain their existing profile-scoped ``load_config`` behavior.
|
||||
"""
|
||||
config = read_raw_config() if raw else load_config()
|
||||
platforms = config.setdefault("platforms", {})
|
||||
if not isinstance(platforms, dict):
|
||||
platforms = {}
|
||||
config["platforms"] = platforms
|
||||
|
||||
platform_config = platforms.setdefault(platform_key, {})
|
||||
if not isinstance(platform_config, dict):
|
||||
platform_config = {}
|
||||
platforms[platform_key] = platform_config
|
||||
|
||||
platform_config[field_key] = value
|
||||
save_config(config)
|
||||
|
||||
|
||||
TERMINAL_CONFIG_ENV_MAP = {
|
||||
"backend": "TERMINAL_ENV",
|
||||
"modal_mode": "TERMINAL_MODAL_MODE",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ from hermes_cli.config import (
|
|||
is_managed,
|
||||
managed_error,
|
||||
read_raw_config,
|
||||
save_config,
|
||||
save_env_value,
|
||||
write_platform_config_field,
|
||||
)
|
||||
|
||||
# display_hermes_home is imported lazily at call sites to avoid ImportError
|
||||
|
|
@ -4648,17 +4648,7 @@ def _runtime_health_lines() -> list[str]:
|
|||
|
||||
def _set_platform_unauthorized_dm_behavior(platform_key: str, behavior: str) -> None:
|
||||
"""Persist a platform-specific unauthorized-DM policy in config.yaml."""
|
||||
cfg = read_raw_config()
|
||||
platforms = cfg.setdefault("platforms", {})
|
||||
if not isinstance(platforms, dict):
|
||||
platforms = {}
|
||||
cfg["platforms"] = platforms
|
||||
platform_cfg = platforms.setdefault(platform_key, {})
|
||||
if not isinstance(platform_cfg, dict):
|
||||
platform_cfg = {}
|
||||
platforms[platform_key] = platform_cfg
|
||||
platform_cfg["unauthorized_dm_behavior"] = behavior
|
||||
save_config(cfg)
|
||||
write_platform_config_field(platform_key, "unauthorized_dm_behavior", behavior, raw=True)
|
||||
|
||||
|
||||
def _setup_standard_platform(platform: dict):
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ from hermes_cli.config import (
|
|||
format_docker_update_message,
|
||||
recommended_update_command_for_method,
|
||||
redact_key,
|
||||
write_platform_config_field,
|
||||
)
|
||||
from hermes_cli.memory_providers import (
|
||||
MemoryProvider,
|
||||
|
|
@ -5006,17 +5007,7 @@ def _messaging_platform_payload(
|
|||
|
||||
|
||||
def _write_platform_enabled(platform_id: str, enabled: bool) -> None:
|
||||
config = load_config()
|
||||
platforms = config.setdefault("platforms", {})
|
||||
if not isinstance(platforms, dict):
|
||||
platforms = {}
|
||||
config["platforms"] = platforms
|
||||
platform_config = platforms.setdefault(platform_id, {})
|
||||
if not isinstance(platform_config, dict):
|
||||
platform_config = {}
|
||||
platforms[platform_id] = platform_config
|
||||
platform_config["enabled"] = enabled
|
||||
save_config(config)
|
||||
write_platform_config_field(platform_id, "enabled", enabled)
|
||||
|
||||
|
||||
_TELEGRAM_ONBOARDING_DEFAULT_URL = "https://setup.hermes-agent.nousresearch.com"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from hermes_cli.config import (
|
|||
save_env_value,
|
||||
save_env_value_secure,
|
||||
sanitize_env_file,
|
||||
write_platform_config_field,
|
||||
_sanitize_env_lines,
|
||||
)
|
||||
|
||||
|
|
@ -255,6 +256,24 @@ class TestSaveAndLoadRoundtrip:
|
|||
reloaded = load_config()
|
||||
assert reloaded["terminal"]["timeout"] == 999
|
||||
|
||||
def test_write_platform_config_field_coerces_nested_platform_maps(self, tmp_path):
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"model: test/custom-model\nplatforms: not-a-map\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
write_platform_config_field(
|
||||
"email",
|
||||
"unauthorized_dm_behavior",
|
||||
"pair",
|
||||
raw=True,
|
||||
)
|
||||
|
||||
saved = yaml.safe_load((tmp_path / "config.yaml").read_text(encoding="utf-8"))
|
||||
assert saved["model"] == "test/custom-model"
|
||||
assert saved["platforms"]["email"]["unauthorized_dm_behavior"] == "pair"
|
||||
|
||||
|
||||
class TestSaveEnvValueSecure:
|
||||
def test_save_env_value_writes_without_stdout(self, tmp_path, capsys):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue