Under a multi-profile host (multiplex, or the desktop backend's per-profile cron ticker), a routed profile B's cron delivery calls apply_media_policy_env(B's config) under B's HERMES_HOME override. The bridge writes B's gateway.strict / media_delivery_allow_dirs / trust_recent_files into the SHARED os.environ whenever the launch config leaves them unset. From then on the launch profile's own unbound media validation, and every launch-profile child built from os.environ (bot relay delivery, slash worker), use B's allowlist and strictness. B's own readers already take B's config under the override (_routed_gateway_cfg,819517fbac), so the write bought nothing. Skip the bridge under a HERMES_HOME override - the exact gate the readers use: there they take the profile's own config, so an env write is never needed and only leaks. Every cron fire binds an override, so the leak is closed for every routed fire. Gated on the override rather than serves_routed_profile(): that predicate is True for the whole process once it multiplexes, which would also skip the launch profile's own override-less bridge (a serve/desktop backend importing gateway.run after activation). Single-profile `hermes cron run` / `hermes send` and gateway boot keep the bridge. Introduced by22f0f22298(Victor Kyriazakos, 2026-08-17, #88631), which added the bridge to cron delivery;819517fbac(Teknium, 2026-09-11) made the readers routed-aware but left the writer unguarded. Found while following up the env-leak note on #120945 / #129575.
121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
"""Shared config→env bridge for media-delivery policy.
|
|
|
|
``validate_media_delivery_path`` reads ``HERMES_MEDIA_DELIVERY_STRICT`` (gateway.strict),
|
|
``HERMES_MEDIA_ALLOW_DIRS`` (gateway.media_delivery_allow_dirs) and
|
|
``HERMES_MEDIA_TRUST_RECENT_FILES`` (gateway.trust_recent_files). Every delivery
|
|
entrypoint (gateway startup, ``hermes cron run``, ``hermes send``) calls
|
|
:func:`apply_media_policy_env` first so standalone paths filter under the gateway's
|
|
policy instead of silently dropping attachments in strict/allowlisted deployments.
|
|
An explicitly-set env var WINS over config.yaml, so shell overrides survive. Under a HERMES_HOME
|
|
override (a served profile's turn or cron fire) nothing is bridged: readers load that profile's config.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import Any, Dict, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_FLAG_ENVS = (("strict", "HERMES_MEDIA_DELIVERY_STRICT"), ("trust_recent_files", "HERMES_MEDIA_TRUST_RECENT_FILES"))
|
|
_ALLOW_DIRS_ENV = "HERMES_MEDIA_ALLOW_DIRS"
|
|
_TRUST_RECENT_SECONDS_ENV = "HERMES_MEDIA_TRUST_RECENT_SECONDS"
|
|
_TRUTHY = frozenset({"1", "true", "yes", "on"})
|
|
|
|
|
|
def _routed_gateway_cfg() -> Optional[Dict[str, Any]]:
|
|
"""``gateway`` section of the ROUTED profile's config when a HERMES_HOME override is active
|
|
(multiplexed turn), else None. The env bridge is one process-wide copy of the launch profile's
|
|
policy, so a secondary's deliveries must read their own config instead of ``os.environ``."""
|
|
from hermes_constants import get_hermes_home_override
|
|
if not get_hermes_home_override():
|
|
return None
|
|
try:
|
|
from hermes_cli.config import load_config_readonly
|
|
gateway_cfg = load_config_readonly().get("gateway")
|
|
except Exception:
|
|
return {}
|
|
return gateway_cfg if isinstance(gateway_cfg, dict) else {}
|
|
|
|
|
|
def media_delivery_strict() -> bool:
|
|
cfg = _routed_gateway_cfg()
|
|
if cfg is not None:
|
|
return bool(cfg.get("strict", False))
|
|
return os.environ.get(_FLAG_ENVS[0][1], "0").strip().lower() in _TRUTHY
|
|
|
|
|
|
def media_delivery_allow_dirs() -> str:
|
|
"""Operator allowlist as the ``os.pathsep``-joined string the validator splits."""
|
|
cfg = _routed_gateway_cfg()
|
|
if cfg is not None:
|
|
return _allow_dirs_str(cfg.get("media_delivery_allow_dirs"))
|
|
return os.environ.get(_ALLOW_DIRS_ENV, "")
|
|
|
|
|
|
def media_delivery_trust_recent() -> bool:
|
|
cfg = _routed_gateway_cfg()
|
|
if cfg is not None:
|
|
return bool(cfg.get("trust_recent_files", True))
|
|
return os.environ.get(_FLAG_ENVS[1][1], "1").strip().lower() not in ("0", "false", "no", "off", "")
|
|
|
|
|
|
def media_delivery_trust_recent_seconds() -> str:
|
|
"""Raw recency window (``""`` = validator default); the caller parses/floors it."""
|
|
cfg = _routed_gateway_cfg()
|
|
if cfg is not None:
|
|
raw = cfg.get("trust_recent_files_seconds")
|
|
return "" if raw is None else str(raw)
|
|
return os.environ.get(_TRUST_RECENT_SECONDS_ENV, "")
|
|
|
|
|
|
|
|
def _load_gateway_cfg(config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
if config is None:
|
|
try:
|
|
from hermes_cli.config import load_config
|
|
|
|
config = load_config() or {}
|
|
except Exception:
|
|
return {}
|
|
gateway_cfg = config.get("gateway", {})
|
|
return gateway_cfg if isinstance(gateway_cfg, dict) else {}
|
|
|
|
|
|
def _set_env_default(env: str, value: str) -> None:
|
|
"""Set ``env`` only when unset/empty and ``value`` is non-empty (env wins)."""
|
|
if value and not os.environ.get(env):
|
|
os.environ[env] = value
|
|
|
|
|
|
def _allow_dirs_str(allow_dirs: Any) -> str:
|
|
if isinstance(allow_dirs, (list, tuple)):
|
|
return os.pathsep.join(str(p) for p in allow_dirs if p)
|
|
return allow_dirs if isinstance(allow_dirs, str) else ""
|
|
|
|
|
|
def apply_media_policy_env(config: Optional[Dict[str, Any]] = None) -> None:
|
|
"""Bridge gateway media-policy settings from config.yaml into the env. Idempotent,
|
|
env-wins, never raises — a bridge failure must not break delivery (validator defaults apply).
|
|
|
|
No-op under a HERMES_HOME override, the same gate the readers use: there they take the
|
|
profile's own config (``_routed_gateway_cfg``), and ``os.environ`` is shared — a routed cron
|
|
fire would hand its allowlist and strictness to the launch profile and its children."""
|
|
try:
|
|
from hermes_constants import get_hermes_home_override
|
|
if get_hermes_home_override():
|
|
return
|
|
gateway_cfg = _load_gateway_cfg(config)
|
|
if not gateway_cfg:
|
|
return
|
|
for key, env in _FLAG_ENVS:
|
|
flag = gateway_cfg.get(key)
|
|
if flag is not None:
|
|
_set_env_default(env, "1" if flag else "0")
|
|
allow_dirs = gateway_cfg.get("media_delivery_allow_dirs")
|
|
if allow_dirs:
|
|
_set_env_default(_ALLOW_DIRS_ENV, _allow_dirs_str(allow_dirs))
|
|
except Exception: # noqa: BLE001 - policy bridge must never break delivery
|
|
logger.debug("apply_media_policy_env failed", exc_info=True)
|