|
@@ -3277,7 +3277,30 @@ async def hermes_standalone_sender_fn(
|
|
|
# Resolved Chatto configuration — populated by ``hermes_validate_config`` so
|
|
# Resolved Chatto configuration — populated by ``hermes_validate_config`` so
|
|
|
# plugin tools can access it without re-reading env vars or duplicating
|
|
# plugin tools can access it without re-reading env vars or duplicating
|
|
|
# ``ChattoConfiguration`` logic.
|
|
# ``ChattoConfiguration`` logic.
|
|
|
-_resolved_chatto_config: ChattoConfiguration | None = None
|
|
|
|
|
|
|
+#
|
|
|
|
|
+# Keyed by the profile the snapshot was validated for, not stored in one slot:
|
|
|
|
|
+# Hermes multiplexes several profiles through a single gateway process and runs
|
|
|
|
|
+# every profile's own validate_config (run_adapters.py, one _create_adapter per
|
|
|
|
|
+# profile), so a single global would be whichever profile validated last — and a
|
|
|
|
|
+# routed cron fire or turn for another profile would then send with the wrong
|
|
|
|
|
+# profile's bot token. The key is the same home key the platform registry scopes
|
|
|
|
|
+# its own entries by, so both agree on what "the serving profile" means.
|
|
|
|
|
+_resolved_chatto_configs: dict[str, ChattoConfiguration] = {}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _profile_scope_key() -> str:
|
|
|
|
|
+ """Registry key of the profile being served right now.
|
|
|
|
|
+
|
|
|
|
|
+ Falls back to a constant when ``hermes_constants`` is unavailable (a bare
|
|
|
|
|
+ standalone sender process): one profile there, so one key loses nothing.
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ from hermes_constants import hermes_home_key
|
|
|
|
|
+
|
|
|
|
|
+ return hermes_home_key()
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ logger.debug("Chatto: no profile scope key available", exc_info=True)
|
|
|
|
|
+ return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
# How similar an ``extra`` key must be to a declared config_key before the
|
|
# How similar an ``extra`` key must be to a declared config_key before the
|
|
@@ -3349,8 +3372,7 @@ def hermes_validate_config(config: PlatformConfig) -> bool:
|
|
|
# Publish for the plugin tools only now: a config we just rejected is not
|
|
# Publish for the plugin tools only now: a config we just rejected is not
|
|
|
# one they should send with, and a rejected call must not overwrite a
|
|
# one they should send with, and a rejected call must not overwrite a
|
|
|
# snapshot an earlier, accepted call left behind.
|
|
# snapshot an earlier, accepted call left behind.
|
|
|
- global _resolved_chatto_config
|
|
|
|
|
- _resolved_chatto_config = resolved
|
|
|
|
|
|
|
+ _resolved_chatto_configs[_profile_scope_key()] = resolved
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
@@ -3662,19 +3684,24 @@ def _chatto_config_for_tools() -> ChattoConfiguration | None:
|
|
|
"""Resolve the Chatto configuration for plugin tool handlers.
|
|
"""Resolve the Chatto configuration for plugin tool handlers.
|
|
|
|
|
|
|
|
Two tiers, most specific first:
|
|
Two tiers, most specific first:
|
|
|
- 1. ``_resolved_chatto_config`` — set by ``hermes_validate_config`` at gateway
|
|
|
|
|
- load; the authoritative snapshot in a running gateway.
|
|
|
|
|
- 2. A fresh read of config.yaml plus the profile's own .env, for the cron and
|
|
|
|
|
- standalone sessions where the gateway load hooks never ran and the dotenv
|
|
|
|
|
- residue has been stripped from ``os.environ``. Mirrors how the built-in
|
|
|
|
|
- send_message tool resolves its pconfig (tools/send_message_tool.py).
|
|
|
|
|
|
|
+ 1. The snapshot ``hermes_validate_config`` published for *this* profile — the
|
|
|
|
|
+ authoritative one in a running gateway.
|
|
|
|
|
+ 2. A fresh read of config.yaml plus the serving profile's own secrets, for
|
|
|
|
|
+ the cron and standalone sessions where the gateway load hooks never ran
|
|
|
|
|
+ and the dotenv residue has been stripped from ``os.environ``. Mirrors how
|
|
|
|
|
+ the built-in send_message tool resolves its pconfig
|
|
|
|
|
+ (tools/send_message_tool.py).
|
|
|
|
|
+
|
|
|
|
|
+ Both tiers are keyed by the serving profile, so a multiplexed gateway hands
|
|
|
|
|
+ each profile its own credentials rather than whichever one was served last.
|
|
|
|
|
|
|
|
Returns ``None`` when neither tier yields a configuration; the caller
|
|
Returns ``None`` when neither tier yields a configuration; the caller
|
|
|
reports that rather than guessing at credentials.
|
|
reports that rather than guessing at credentials.
|
|
|
"""
|
|
"""
|
|
|
- global _resolved_chatto_config
|
|
|
|
|
- if _resolved_chatto_config is not None:
|
|
|
|
|
- return _resolved_chatto_config
|
|
|
|
|
|
|
+ scope_key = _profile_scope_key()
|
|
|
|
|
+ snapshot = _resolved_chatto_configs.get(scope_key)
|
|
|
|
|
+ if snapshot is not None:
|
|
|
|
|
+ return snapshot
|
|
|
|
|
|
|
|
# config.yaml's own ``extra`` block, so a deployment configured there — and
|
|
# config.yaml's own ``extra`` block, so a deployment configured there — and
|
|
|
# nowhere in .env — is honoured outside the gateway process too.
|
|
# nowhere in .env — is honoured outside the gateway process too.
|
|
@@ -3685,29 +3712,35 @@ def _chatto_config_for_tools() -> ChattoConfiguration | None:
|
|
|
pconfig = load_gateway_config().platforms.get(
|
|
pconfig = load_gateway_config().platforms.get(
|
|
|
GatewayPlatform(ChattoConstants.PLATFORM_NAME)
|
|
GatewayPlatform(ChattoConstants.PLATFORM_NAME)
|
|
|
)
|
|
)
|
|
|
- _resolved_chatto_config = ChattoConfiguration(
|
|
|
|
|
- pconfig=_pconfig_or_empty(pconfig)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ resolved = ChattoConfiguration(pconfig=_pconfig_or_empty(pconfig))
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.warning("Chatto tools: could not read gateway config: %s", exc)
|
|
logger.warning("Chatto tools: could not read gateway config: %s", exc)
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
# In a fresh cron scope the dotenv residue is stripped from os.environ, so
|
|
# In a fresh cron scope the dotenv residue is stripped from os.environ, so
|
|
|
- # ChattoConfiguration's env layer sees nothing. build_profile_secret_scope
|
|
|
|
|
- # re-reads the profile's own .env directly — it works in gateway, cron and
|
|
|
|
|
- # CLI contexts alike — so every field that is not an explicit user value
|
|
|
|
|
- # gets a second chance from there.
|
|
|
|
|
|
|
+ # ChattoConfiguration's env layer sees nothing. The secrets then come from
|
|
|
|
|
+ # the scope Hermes installed for the profile being served when there is one —
|
|
|
|
|
+ # that mapping is that profile's authoritative secret set, and rebuilding
|
|
|
|
|
+ # one from the launch home would answer with another profile's credentials.
|
|
|
|
|
+ # With no scope installed (CLI, bare standalone sender) the profile's own
|
|
|
|
|
+ # .env is the right source, and build_profile_secret_scope reads it
|
|
|
|
|
+ # directly in gateway, cron and CLI contexts alike.
|
|
|
try:
|
|
try:
|
|
|
- from agent.secret_scope import build_profile_secret_scope
|
|
|
|
|
- from hermes_constants import get_hermes_home
|
|
|
|
|
|
|
+ from agent.secret_scope import current_secret_scope
|
|
|
|
|
+
|
|
|
|
|
+ profile_secrets = current_secret_scope()
|
|
|
|
|
+ if profile_secrets is None:
|
|
|
|
|
+ from agent.secret_scope import build_profile_secret_scope
|
|
|
|
|
+ from hermes_constants import get_hermes_home
|
|
|
|
|
|
|
|
- profile_secrets = dict(build_profile_secret_scope(get_hermes_home()))
|
|
|
|
|
|
|
+ profile_secrets = dict(build_profile_secret_scope(get_hermes_home()))
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
logger.warning("Chatto tools: profile secret scope build failed: %s", exc)
|
|
logger.warning("Chatto tools: profile secret scope build failed: %s", exc)
|
|
|
profile_secrets = {}
|
|
profile_secrets = {}
|
|
|
|
|
|
|
|
- _resolved_chatto_config.overlay_profile_secrets(profile_secrets)
|
|
|
|
|
- return _resolved_chatto_config
|
|
|
|
|
|
|
+ resolved.overlay_profile_secrets(profile_secrets)
|
|
|
|
|
+ _resolved_chatto_configs[scope_key] = resolved
|
|
|
|
|
+ return resolved
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pconfig_or_empty(pconfig: PlatformConfig | None) -> PlatformConfig:
|
|
def _pconfig_or_empty(pconfig: PlatformConfig | None) -> PlatformConfig:
|