"""Secret-scrub policy for Hermes child processes: pure data + predicates for which env
names are Hermes-managed credentials. The env *builders* applying it (``_make_run_env``,
``_sanitize_subprocess_env``, ``hermes_subprocess_env``) live in ``tools.environments.local``."""

import functools
import os
from typing import Optional

# Prefix a caller uses in ``extra_env`` to force a blocklisted var through.
_HERMES_PROVIDER_ENV_FORCE_PREFIX = "_HERMES_FORCE_"

# Hermes-managed AWS *inference* credentials for ``auth_type="aws_sdk"`` (Bedrock):
# only the Bedrock bearer token, which no aws/terraform/boto3 toolchain uses. The
# general AWS chain stays inheritable on purpose — the local terminal is the user's
# trusted operator shell (SECURITY.md §3.2) and env_passthrough can never re-allow a
# blocklisted name (GHSA-rhgp-j443-p4rf), so blocking it would be unrecoverable.
_AWS_SDK_CREDENTIAL_ENV_VARS = frozenset({"AWS_BEARER_TOKEN_BEDROCK"})

_STATIC_PROVIDER_ENV_BLOCKLIST = frozenset({
    "OPENAI_BASE_URL", "OPENAI_API_KEY", "OPENAI_API_BASE", "OPENAI_ORG_ID",
    "OPENAI_ORGANIZATION", "OPENROUTER_API_KEY", "ANTHROPIC_BASE_URL",
    "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "LLM_MODEL", "GOOGLE_API_KEY",
    # Path to a GCP service-account JSON, not a bare key, so OPTIONAL_ENV_VARS
    # marks it password=False and the registry loop skips it.
    "VERTEX_CREDENTIALS_PATH", "GOOGLE_APPLICATION_CREDENTIALS", "DEEPSEEK_API_KEY",
    "MISTRAL_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY", "PERPLEXITY_API_KEY",
    "COHERE_API_KEY", "FIREWORKS_API_KEY", "XAI_API_KEY", "HELICONE_API_KEY",
    "PARALLEL_API_KEY", "FIRECRAWL_API_KEY", "FIRECRAWL_API_URL",
    "TELEGRAM_HOME_CHANNEL", "TELEGRAM_HOME_CHANNEL_NAME", "DISCORD_HOME_CHANNEL",
    "DISCORD_HOME_CHANNEL_NAME", "DISCORD_REQUIRE_MENTION",
    "DISCORD_FREE_RESPONSE_CHANNELS", "DISCORD_AUTO_THREAD", "SLACK_HOME_CHANNEL",
    "SLACK_HOME_CHANNEL_NAME", "SLACK_ALLOWED_USERS", "WHATSAPP_ENABLED",
    "WHATSAPP_MODE", "WHATSAPP_ALLOWED_USERS", "SIGNAL_HTTP_URL", "SIGNAL_ACCOUNT",
    "SIGNAL_ALLOWED_USERS", "SIGNAL_GROUP_ALLOWED_USERS", "SIGNAL_HOME_CHANNEL",
    "SIGNAL_HOME_CHANNEL_NAME", "SIGNAL_IGNORE_STORIES", "HASS_TOKEN", "HASS_URL",
    "EMAIL_ADDRESS", "EMAIL_PASSWORD", "EMAIL_IMAP_HOST", "EMAIL_SMTP_HOST",
    "EMAIL_HOME_ADDRESS", "EMAIL_HOME_ADDRESS_NAME", "HERMES_DASHBOARD_SESSION_TOKEN",
    "GATEWAY_ALLOWED_USERS", "GH_TOKEN", "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH",
    "GITHUB_APP_INSTALLATION_ID", "MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET",
    "DAYTONA_API_KEY", "GATEWAY_RELAY_ID", "GATEWAY_RELAY_SECRET",
    "GATEWAY_RELAY_DELIVERY_KEY", "VERCEL_OIDC_TOKEN", "VERCEL_TOKEN",
    "VERCEL_PROJECT_ID", "VERCEL_TEAM_ID",
    # Keys the OAuth provider profiles (nous, qwen-oauth) also accept when pasted. The auth
    # registry mirrors env_vars only for api_key profiles, and discovering the provider plugins
    # from here, at import, would re-mirror them over a plugin's own registry entry.
    "NOUS_API_KEY", "QWEN_API_KEY",
    # Hermes' own secrets read in code: the anonymous-inference secret, dashboard auth
    # (basic, OIDC, drain) and the Google Meet realtime key.
    "HERMES_ANON_API_SECRET", "HERMES_DASHBOARD_BASIC_AUTH_PASSWORD",
    "HERMES_DASHBOARD_BASIC_AUTH_SECRET", "HERMES_DASHBOARD_DRAIN_SECRET",
    "HERMES_DASHBOARD_OIDC_CLIENT_SECRET", "HERMES_MEET_REALTIME_KEY",
})


def _build_provider_env_blocklist() -> frozenset:
    """Derive the blocklist from provider, tool, and gateway config."""
    blocked: set[str] = set(_STATIC_PROVIDER_ENV_BLOCKLIST)
    try:
        from hermes_cli.auth import PROVIDER_REGISTRY
        for pconfig in PROVIDER_REGISTRY.values():
            blocked.update(pconfig.api_key_env_vars)
            if pconfig.auth_type == "aws_sdk":
                blocked.update(_AWS_SDK_CREDENTIAL_ENV_VARS)
            if pconfig.base_url_env_var:
                blocked.add(pconfig.base_url_env_var)
    except ImportError:
        pass
    try:
        from hermes_cli.config import OPTIONAL_ENV_VARS
        for name, metadata in OPTIONAL_ENV_VARS.items():
            category = metadata.get("category")
            if category in {"tool", "messaging"} or (
                    category == "setting" and metadata.get("password")):
                blocked.add(name)
    except ImportError:
        pass
    # CLAUDE_CODE_OAUTH_TOKEN (via the anthropic registry entry) belongs to the user's
    # Claude Code install, not Hermes: stripping it made agent-spawned ``claude`` CLIs
    # fall through to the shared Keychain / ~/.claude store and, on auth failure, wipe
    # it — logging the user out. BUZZ_* is deliberately NOT discarded: this list feeds
    # every scrub surface, so an import-time discard would leak BUZZ_PRIVATE_KEY into
    # non-terminal children; the Buzz carve-out is terminal-only and context-gated
    # (``_is_terminal_first_party_env``).
    # It is set and owned by the user's Claude Code install (subscription OAuth), not a Hermes-managed
    # inference credential — Claude subscription auth is not a working Hermes provider path. It arrives via
    # the registry loop above (anthropic api_key_env_vars), so remove it explicitly. See #55878.
    blocked.discard("CLAUDE_CODE_OAUTH_TOKEN")
    # BUZZ_* is deliberately NOT discarded here, even for Buzz-managed agents (BUZZ_MANAGED_AGENT set by the
    # buzz-acp harness). See #76243, #78026, #78065, #78511.
    return frozenset(blocked)


def _build_adapter_secret_env() -> frozenset:
    """Secrets the messaging adapters declare, process-wide: core ``password`` messaging entries
    of OPTIONAL_ENV_VARS, the bundled platform plugin manifests' secret entries, and the
    secret-named keys the gateway env-override table reads (WEIXIN_TOKEN, FEISHU_ENCRYPT_KEY, ...).
    Declared names only: a user's own ``SLACK_USER_TOKEN`` or ``LOCAL_LLM_API_KEY`` is not Hermes's.
    A profile's user-installed platform plugins are per home: :func:`_home_adapter_secret_env`.
    Nothing here fails soft: an unreadable bundled manifest or env table fails the import rather
    than dropping its secrets from the policy."""
    from hermes_cli.config import (
        CORE_DECLARED_ENV_NAMES, OPTIONAL_ENV_VARS, PLATFORM_SECRET_ENV_SUFFIXES, platform_manifest_secret_envs)
    from hermes_cli.profile_channels import config_env_table_keys
    # Read in code only, declared nowhere else: the Microsoft Graph app secret and webhook
    # clientState, and the QQ bot's speech-to-text key.
    names: set[str] = {"MSGRAPH_CLIENT_SECRET", "MSGRAPH_WEBHOOK_CLIENT_STATE", "QQ_STT_API_KEY"}
    names.update(name.upper() for name, meta in OPTIONAL_ENV_VARS.items()
                 if name in CORE_DECLARED_ENV_NAMES
                 and meta.get("category") == "messaging" and meta.get("password"))
    names |= platform_manifest_secret_envs(source="bundled", strict=True)
    names.update(n.upper() for n in config_env_table_keys() if n.upper().endswith(PLATFORM_SECRET_ENV_SUFFIXES))
    return frozenset(names)


# Provider blocklist first: it imports hermes_cli.auth before hermes_cli.config, whose import
# discovers provider plugins that expect a fully initialized auth registry.
_PROVIDER_ENV_BLOCKLIST = _build_provider_env_blocklist()
_ADAPTER_SECRET_ENV = _build_adapter_secret_env()
_HERMES_PROVIDER_ENV_BLOCKLIST = _PROVIDER_ENV_BLOCKLIST | _ADAPTER_SECRET_ENV

_HOME_ADAPTER_SECRET_CACHE: dict[str, tuple] = {}


def _home_adapter_secret_env() -> frozenset:
    """Secrets declared by the bound profile's own user-installed platform plugins. Per home, not
    process-wide: under multiplex profile A's plugin must neither strip a same-named value from
    profile B's children nor be missing from A's. Cached per home and keyed on every manifest's
    file signature, so an edit, replacement or deletion takes effect on the next spawn. A plugin
    manifest under ``plugins/platforms/`` that cannot be read raises. A partial scan (a plugin dir
    or flat manifest unreadable or unparsable) keeps the names this home already had and is not
    cached, so a failed discovery never releases a known denial and recovery is seen at once."""
    from hermes_cli.config import platform_manifest_secret_scan, platform_manifest_stamp
    from hermes_constants import get_hermes_home, hermes_home_key
    home = get_hermes_home()
    key, stamp = hermes_home_key(home), platform_manifest_stamp(home)
    cached = _HOME_ADAPTER_SECRET_CACHE.get(key)
    if cached is None or cached[0] is None or cached[0] != stamp:
        names, complete = platform_manifest_secret_scan(home)
        names -= _ADAPTER_SECRET_ENV
        if not complete and cached is not None:
            names |= cached[1]
        cached = (stamp if complete else None, names)
        _HOME_ADAPTER_SECRET_CACHE[key] = cached
    return cached[1]


def _registry_adapter_secret_env() -> frozenset:
    """Secret-named ``required_env`` of the adapters registered in the current profile scope.
    Tier 2 only: ``required_env`` is an unchecked setup list, so a plugin naming OPENAI_API_KEY
    must not strip it from credentialed children. No blanket fallback: a registry error surfaces
    instead of an empty (fail-open) set."""
    from gateway.platform_registry import platform_registry
    from hermes_cli.config import PLATFORM_SECRET_ENV_SUFFIXES
    return frozenset(n.upper() for n in platform_registry.required_env_names()
                     if n.upper().endswith(PLATFORM_SECRET_ENV_SUFFIXES))


def _registered_adapter_secret_env() -> frozenset:
    """Per-call adapter secrets: the bound profile's user-plugin declarations (Tier 1, see
    :func:`_home_adapter_secret_env`) plus the registered adapters' (Tier 2,
    :func:`_registry_adapter_secret_env`)."""
    return _registry_adapter_secret_env() | _home_adapter_secret_env()


def _is_provider_env_blocklisted(name: str, _registered: "frozenset | None" = None) -> bool:
    """``name`` is a blocklisted provider/tool credential or adapter secret, matched the way the
    platform's environment resolves names: exact plus case-folded. On Windows the environment
    block is case-insensitive, so ``openai_api_key`` IS ``OPENAI_API_KEY``; consistent with
    ``_is_hermes_internal_secret``, which already folds (``key.upper()``). Loops pass
    ``_registered`` so the registry is read once per env, not once per key."""
    upper = name.upper()
    if name in _HERMES_PROVIDER_ENV_BLOCKLIST or upper in _HERMES_PROVIDER_ENV_BLOCKLIST:
        return True
    return upper in (_registered_adapter_secret_env() if _registered is None else _registered)


# First-party platform credentials (``BUZZ_*``, driving the platform-mandated ``buzz``
# CLI) carved out of the TERMINAL scrub only (``_make_run_env``,
# ``_sanitize_subprocess_env``); execute_code, hermes_subprocess_env, docker and
# env_passthrough registration stay sealed (GHSA-rhgp-j443-p4rf). CONTEXT-GATED via
# ``_buzz_terminal_context_active``: a Telegram/CLI/cron session on a host that also
# runs a Buzz gateway must not get the signing key. Values are used directly, never
# scope-resolved (UnscopedSecretError under multiplex); the snapshot treats them as
# profile-scoped. Prefix-based so future BUZZ_* names need no code change.
# First-party platform credentials the agent's own platform adapters need in terminal children (e.g. the
# ``BUZZ_*`` vars for the Buzz messaging platform, which drive the platform-mandated ``buzz`` CLI:
# BUZZ_PRIVATE_KEY, BUZZ_AUTH_TAG, BUZZ_RELAY_URL, and the other BUZZ_* names). These are the agent's OWN
# credentials — a Buzz community agent is expected to operate the ``buzz`` CLI — so they are carved out of
# the terminal scrub. CONTEXT-GATED: the carve-out applies ONLY when this process/session is actually
# operating as a Buzz agent — either the process is a Buzz-ACP managed agent (``BUZZ_MANAGED_AGENT`` is set,
# only by Buzz Desktop's buzz-acp harness; see #76243 / #78511) or the current session's platform is
# ``buzz`` (the gateway's ``HERMES_SESSION_PLATFORM`` ContextVar; concurrency safe under a multi-session
# host). A Telegram/CLI/cron session on a host that also runs a Buzz gateway does NOT get BUZZ_PRIVATE_KEY
# in its terminal children — blanket passthrough of a signing key to every terminal child on the host would
# be wrong (maintainer triage note on #76243: don't expose the key to unrelated shell commands).
# ``_sanitize_subprocess_env`` is also consumed by search workers (e.g. the ddgs web-search subprocess), the
# computer-use driver binary, and user-script runners (bang ``!`` commands, quick commands, cron scripts,
# webhook-filter scripts), so those children receive the vars too — matching the approved background/PTY
# scope. Every other surface stays sealed — execute_code scrubbing, :func:`hermes_subprocess_env` (browser /
# TUI host / copilot-executor spawns), docker children, and ``env_passthrough`` registration (skills/config
# still cannot register these names). The GHSA-rhgp-j443-p4rf seal is preserved because no registration path
# is opened; this is a scrub-path exemption, not an allowlist addition. First-party matches use the merged
# env value directly — they are the process's own env values and are never scope-resolved (a profile secret
# scope under multiplex would otherwise raise UnscopedSecretError at passthrough-resolution call sites);
# only skill/config passthrough names resolve through the profile secret scope. The snapshot mechanism
# treats these names like profile-scoped passthrough names (see
# ``LocalEnvironment._additional_profile_scoped_passthrough_names``) so they never persist in the shared
# terminal snapshot across profiles. Contrast with CLAUDE_CODE_OAUTH_TOKEN above, which is discarded from
# the blocklist entirely because it is NOT a Hermes credential; these ARE Hermes-managed first-party
# platform credentials, so they stay IN the blocklist for every non-terminal surface. See issue #78026 (Buzz
# agents could not use ``buzz`` from the terminal tool) and #76243 (Buzz Desktop managed agent wakes but
# cannot reply).
_TERMINAL_FIRST_PARTY_ENV_PREFIXES = ("BUZZ_",)


def _matches_terminal_first_party_prefix(name: str) -> bool:
    """Pure name check (``BUZZ_*``), regardless of session context — the snapshot
    exclusion must stay conservative even when the carve-out is inactive.
    Case-folded: on Windows the env block is case-insensitive, so a
    lowercase-stored ``buzz_private_key`` IS the credential; it needs the
    carve-out (and the snapshot exclusion) just like the canonical name."""
    return name.upper().startswith(_TERMINAL_FIRST_PARTY_ENV_PREFIXES)


def _buzz_terminal_context_active() -> bool:
    """True when this process/session operates as a Buzz agent: ``BUZZ_MANAGED_AGENT`` in
    the process env (set only by Buzz Desktop's buzz-acp harness), or the live session's
    platform is ``buzz`` via the gateway ContextVar — authoritative under a concurrent
    multi-session host, so a sibling Telegram session resolves its OWN platform.

    Gateway / CLI / cron / kanban processes never carry it. See #76243.
    """
    if os.environ.get("BUZZ_MANAGED_AGENT"):
        return True
    try:
        from gateway.session_context import get_session_env

        return get_session_env("HERMES_SESSION_PLATFORM", "").strip().lower() == "buzz"
    except Exception:
        return False


def _is_terminal_first_party_env(name: str) -> bool:
    """``name`` is a first-party platform credential (``BUZZ_*``) AND the current
    process/session context entitles it to reach terminal children."""
    return _matches_terminal_first_party_prefix(name) and _buzz_terminal_context_active()


# Active-venv markers that must NOT leak: VIRTUAL_ENV/CONDA_PREFIX make uv/poetry sync
# ANOTHER project's deps into the Hermes venv (still reachable via PATH, so stripping
# is safe); PYTHONHOME redirects a child interpreter's stdlib to the Hermes venv
# (version-mismatch crashes). PYTHONPATH is handled separately (Hermes-owned entries only).
# The gateway runs inside its own venv, so its process environment carries VIRTUAL_ENV (and possibly
# CONDA_PREFIX). If those leak into commands the agent runs against OTHER Python projects, tools like
# ``uv``/``poetry`` treat the inherited value as the active environment and build/sync that other project's
# dependencies into the Hermes venv path instead of the project's own ``.venv`` — silently clobbering the
# Hermes environment (e.g. a project pinned to a different Python version overwrites it and breaks the
# gateway). PYTHONHOME is included because a gateway-inherited value redirects the standard-library search
# of ANY child interpreter — including unrelated system/venv Pythons — to the Hermes venv's stdlib, which
# crashes with version-mismatch errors before a child script even imports a package (#75018). Hermes itself
# treats PYTHONHOME as contamination in its own child processes (managed_uv.py, sqlite_runtime.py), so
# stripping it from subprocess envs is consistent. Users who need PYTHONHOME for a specific child can set it
# explicitly in the command. PYTHONPATH is NOT included here — it's handled by
# _strip_hermes_owned_pythonpath() which removes only Hermes-owned entries, preserving user-set paths.
_ACTIVE_VENV_MARKER_VARS = ("VIRTUAL_ENV", "CONDA_PREFIX", "PYTHONHOME")


def _is_hermes_internal_secret(key: str) -> bool:
    """True for Hermes-internal secrets injected under *dynamic* names the static
    blocklist cannot enumerate: ``AUXILIARY_<TASK>_API_KEY``/``_BASE_URL`` (per-task
    side-LLM credentials) and ``GATEWAY_RELAY_*_SECRET``/``_KEY``/``_TOKEN`` (relay
    auth; non-secret routing hints stay visible). Stripped on every spawn path
    regardless of env_passthrough registration or ``inherit_credentials``."""
    upper = key.upper()
    if upper.startswith("AUXILIARY_") and upper.endswith(("_API_KEY", "_BASE_URL")):
        return True
    return upper.startswith("GATEWAY_RELAY_") and upper.endswith(("_SECRET", "_KEY", "_TOKEN"))


# Authorization gates: the env names platform adapters read to decide WHO may talk to the
# agent (allow/deny lists, allow-all opt-ins, bot policy, channel scoping). Not credentials, so
# no secret scrub touches them, and most profiles' ``.env`` files do not define them, so the
# child's own dotenv load never overwrites an inherited value: a child spawned FOR profile B
# from a process that loaded profile A's gates (or a unit-file ``Environment=``) would enforce
# A's channel/user/role list as its own (#113270). The suffix is matched by shape so a gate
# added to any adapter is covered without a second edit, but ONLY under a platform prefix
# (``DISCORD_``, ``GATEWAY_``, a plugin adapter's name): an operator's own ``DEMO_ALLOWED_SENDER``
# is script data, not a Hermes gate, and deleting it by name shape broke routed ``no_agent``
# cron scripts (#119539). ``HERMES_*`` never counts (``HERMES_MEDIA_ALLOW_DIRS``,
# ``HERMES_ALLOW_PRIVATE_URLS`` are process settings, not adapter gates).
_PROFILE_GATE_ENV_MARKERS = (
    "_ALLOWED_", "_ALLOW_ALL_", "_ALLOW_FROM", "_ALLOW_BOTS", "_ALLOW_PUBLIC_", "_IGNORED_CHANNELS",
    "_NO_THREAD_CHANNELS", "_FREE_RESPONSE_CHANNELS", "_BACKFILL_CHANNELS", "_GROUP_ALLOWED",
)
# Gate owners that are not a ``Platform`` value: the cross-platform pairing gate and adapters whose
# env prefix differs from their platform name (``qqbot`` reads ``QQ_*``).
_EXTRA_GATE_ENV_PREFIXES = frozenset({"GATEWAY", "QQ"})


@functools.lru_cache(maxsize=1)
def _static_gate_env_prefixes() -> frozenset:
    """Built-in ``Platform`` values plus bundled platform plugins (directory names and manifest
    aliases) — fixed for the life of the process, so scanned once."""
    names = set(_EXTRA_GATE_ENV_PREFIXES)
    try:
        from gateway.config import Platform
        names.update(m.value for m in Platform.__members__.values())
        bundled, aliases = Platform._scan_bundled_plugin_platforms()
        names.update(bundled)
        names.update(aliases)
    except Exception:  # noqa: BLE001 — a broken gateway import must not disable the gate strip
        pass
    return frozenset(str(n).upper().replace("-", "_") for n in names if n)


def _platform_gate_env_prefixes() -> frozenset:
    """Upper-cased owners of authorization gates: the static set plus runtime-registered plugin
    adapters (``platform_registry``, dynamic and profile-scoped, so read per call — a cheap set
    union under the registry lock)."""
    names = set(_static_gate_env_prefixes())
    try:
        from gateway.platform_registry import platform_registry
        names.update(str(n).upper().replace("-", "_") for n in platform_registry.registered_names() if n)
    except Exception:  # noqa: BLE001
        pass
    return frozenset(names)


def is_profile_gate_env(name: str, _prefixes: Optional[frozenset] = None) -> bool:
    """True for a platform authorization gate (``DISCORD_ALLOWED_CHANNELS``, ``TELEGRAM_ALLOW_ALL_USERS``,
    ``GATEWAY_ALLOWED_USERS``, ``WHATSAPP_GROUP_ALLOW_FROM`` ...) — profile-scoped policy a child acting
    for ANOTHER profile must never inherit. A gate is a platform prefix AND a gate-shaped suffix;
    an operator variable that merely contains ``_ALLOWED_`` is not one."""
    upper = name.upper()
    if upper.startswith("HERMES_") or upper.startswith("_"):
        return False
    if not any(marker in upper for marker in _PROFILE_GATE_ENV_MARKERS):
        return False
    prefixes = _prefixes if _prefixes is not None else _platform_gate_env_prefixes()
    return any(upper.startswith(prefix + "_") for prefix in prefixes)


def strip_profile_gate_env(env: dict) -> dict:
    """Drop every authorization gate from *env* in place (see :func:`is_profile_gate_env`)."""
    prefixes = _platform_gate_env_prefixes()
    for key in [k for k in env if is_profile_gate_env(k, prefixes)]:
        del env[key]
    return env


def _plugin_terminal_env_strip_keys() -> frozenset:
    """Credential env keys owned by plugin-registered terminal backends (Tier-1:
    stripped from every spawned subprocess). Computed at call time because plugins
    register after import; fail-soft to empty."""
    try:
        from agent.terminal_env_registry import plugin_strip_env_keys

        return plugin_strip_env_keys()
    except Exception:
        return frozenset()


# Tier-1 secrets: stripped from EVERY spawned subprocess even under inherit_credentials
# (claude/codex/gemini). Not provider credentials — no child needs them and they are the
# highest-value secrets to keep from a compromised dependency. Provider keys = Tier 2.
_ALWAYS_STRIP_KEYS: frozenset[str] = frozenset({
    # GitHub auth
    "GH_TOKEN", "GITHUB_TOKEN", "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH",
    "GITHUB_APP_INSTALLATION_ID",
    # Gateway / messaging bot tokens and access control
    "TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN", "SLACK_BOT_TOKEN", "SLACK_APP_TOKEN",
    "SLACK_SIGNING_SECRET", "GATEWAY_ALLOWED_USERS", "GATEWAY_ALLOW_ALL_USERS",
    # Gateway relay auth triplet. _SECRET/_DELIVERY_KEY are also matched by
    # _is_hermes_internal_secret, but _ID has no secret suffix, so it must be
    # enumerated here to stay stripped on the inherit_credentials=True path.
    "GATEWAY_RELAY_ID", "GATEWAY_RELAY_SECRET", "GATEWAY_RELAY_DELIVERY_KEY",
    "HASS_TOKEN", "EMAIL_PASSWORD", "HERMES_DASHBOARD_SESSION_TOKEN",
    # Dashboard auth: the basic-auth password and session-signing secret, the OIDC client
    # secret and the drain bearer. They let a holder mint or forge dashboard sessions, and no
    # child (credentialed CLIs included) consumes them.
    "HERMES_DASHBOARD_BASIC_AUTH_PASSWORD", "HERMES_DASHBOARD_BASIC_AUTH_SECRET",
    "HERMES_DASHBOARD_OIDC_CLIENT_SECRET", "HERMES_DASHBOARD_DRAIN_SECRET",
    # Remote-compute / infrastructure secrets
    "MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET", "DAYTONA_API_KEY",
}) | _ADAPTER_SECRET_ENV  # every declared adapter secret is Tier 1, like the bot tokens above
_ALWAYS_STRIP_FOLDED: frozenset[str] = frozenset(k.upper() for k in _ALWAYS_STRIP_KEYS)
