Skip to content

fix(qqbot): resolve credentials under the active profile secret scope - #60420

Closed
Da7-Tech wants to merge 3 commits into
NousResearch:mainfrom
Da7-Tech:fix/qqbot-get-secret-profile-isolation
Closed

fix(qqbot): resolve credentials under the active profile secret scope#60420
Da7-Tech wants to merge 3 commits into
NousResearch:mainfrom
Da7-Tech:fix/qqbot-get-secret-profile-isolation

Conversation

@Da7-Tech

@Da7-Tech Da7-Tech commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The QQ (qqbot) gateway adapter read its per-profile settings — QQ_APP_ID, QQ_CLIENT_SECRET, the QQ_STT_* speech-to-text config, and the QQ_ALLOW_ALL_USERS policy flag — through raw os.getenv. That bypasses the active profile secret scope, so in multiplex mode a secondary profile whose secret lives in its own .env (loaded as an isolated scope, not into os.environ) silently falls back to the default/primary profile's value — the same cross-profile credential collision fixed for the WeChat/weixin adapter in #59662.

This routes those reads through a small scope-aware resolver, _resolve_qq_secret:

  • A profile secret scope is installed (every secondary multiplex profile is constructed and handled inside _profile_runtime_scope, as is each per-turn inbound message) → read from the scope via get_secret, so profiles never see each other's os.environ values.
  • No scope installed → fall back to os.environ. This fallback is deliberate: the primary/active profile is constructed without a scope (gateway/run.py) while multiplexing is active, so a bare get_secret would raise UnscopedSecretError and break the active profile's startup. The resolver mirrors the existing scope-aware gateway.config._getenv.

Because secondary profiles are always scoped during construction and runtime (contextvars propagate to their asyncio tasks), they never reach the os.environ fallback — no leak. Single-profile deployments are unaffected (multiplex inactive → os.environ, exactly as before).

GATEWAY_ALLOW_ALL_USERS and the network proxy vars are intentionally left as raw os.getenv — they are deployment-global settings, not per-profile secrets.

Related Issue

Same class as #59662 (the WeChat/weixin adapter). No qqbot-specific issue exists yet.

Type of Change

  • Security fix

Changes Made

  • gateway/platforms/qqbot/adapter.py: add _resolve_qq_secret (scope-aware, with os.environ fallback for the unscoped active profile) and route all per-profile QQ_* reads through it. Returns str, so the STT base_url/model are no longer Optional-typed.
  • tests/gateway/test_qqbot_credential_isolation.py: new regression tests — scope-wins-over-environ, two-profile isolation, single-profile fallback, explicit-config precedence, STT key scoping, and active-profile-no-scope construction (proves the fail-closed case does not raise).

How to Test

Run: pytest tests/gateway/test_qqbot_credential_isolation.py -q

Checklist

  • Conventional Commit message
  • Only changes related to this fix
  • Tests pass locally
  • Added tests for the change

Review follow-up (hermes-sweeper)

Commit 7875c0a20 extends the same scoped resolution to the three flagged paths:

  • Gateway authorization (gateway/authz_mixin.py): the per-platform allow-all flag, per-platform/group allowlists, and allow-bots reads now go through the scope-aware gateway.config._getenv. The deployment-global GATEWAY_ALLOW_ALL_USERS / GATEWAY_ALLOWED_USERS reads intentionally remain raw os.getenv. Because these checks are platform-generic, the fix applies to every own-policy platform (not just QQ); with no scope installed _getenv is byte-identical to os.getenv, so single-profile deployments are unaffected.
  • Startup validation (gateway/run.py::_own_policy_open_startup_violation): the per-platform dm/group policy and allow-all opt-in resolve via _getenv; the secondary-profile caller already runs inside _profile_runtime_scope, so each profile's own opt-in is honored.
  • Direct send (tools/send_message_tool.py::_send_qqbot): the QQ_APP_ID / QQ_CLIENT_SECRET fallbacks now honor the active profile scope.

New end-to-end tests in tests/gateway/test_qqbot_scope_paths.py cover all three paths (scoped value wins over environ, a profile without its own opt-in does not inherit the primary's environ opt-in, unscoped single-profile behavior unchanged), and the STT suite now asserts QQ_STT_BASE_URL / QQ_STT_MODEL scoping alongside the API key. All five scoped-behavior tests fail on the previous head and pass on this one.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/gateway Gateway runner, session dispatch, delivery platform/qqbot QQ Bot adapter area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data P2 Medium — degraded but workaround exists labels Jul 7, 2026
@egilewski

Copy link
Copy Markdown
Contributor

looks mergeable

Security evidence:

  • trust boundary: in gateway multiplex mode, each profile's QQ bot credentials must stay within that profile's secret scope instead of falling back to process-global environment values from another profile.
  • source/sink/invariant: QQAdapter constructor credentials and _resolve_stt_config() now read QQ_APP_ID, QQ_CLIENT_SECRET, and QQ_STT_* through agent.secret_scope.get_secret, while explicit config.extra values still take precedence and process-global QQ policy/proxy settings remain unchanged.
  • current-main reproduction: a run-root probe imported gateway.platforms.qqbot.adapter and agent.secret_scope from the current-main worktree and showed that, with multiplex active and a scoped profile mapping installed, QQ app id, client secret, and STT fallback config still came from global os.environ.
  • PR-head or patch-replay validation: the same probe imported both modules from the PR-head worktree and showed the constructor and STT fallback resolve the scoped profile values; a second PR-head probe showed gateway.config._apply_env_overrides also stores scoped QQ credentials in Platform.QQBOT.extra when a profile secret scope is active.
  • positive/negative cases: the new regression test covers scoped values winning over global env, two-profile isolation, single-profile env fallback, explicit config precedence, and STT API-key scoping, and the existing QQ adapter suite still passes.
  • residual bypass search: the changed QQ credential fallback paths are limited to the adapter constructor and STT config; review also checked the config env-override path that can populate config.extra, and process-global access-policy/proxy env reads were left out of scope because they are not per-profile secrets.
  • reviewer validation: CodeRabbit completed with no findings in the clean-pass flow.

Signed: GPT-5.5-xhigh in Codex

@Da7-Tech
Da7-Tech force-pushed the fix/qqbot-get-secret-profile-isolation branch from 3d82651 to 2e97fca Compare July 7, 2026 20:42
@Da7-Tech Da7-Tech changed the title fix(qqbot): resolve credentials via get_secret for profile isolation fix(qqbot): resolve credentials under the active profile secret scope Jul 7, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing a real multiplex credential-isolation gap. Current main still reads QQ adapter credentials and STT fallback values directly from os.getenv (gateway/platforms/qqbot/adapter.py:204-207, 2198-2204) even though secondary adapters are created under _profile_runtime_scope (gateway/run.py:8657-8689).

Problems

  • The new QQ_ALLOW_ALL_USERS resolver only changes the adapter intake check. Gateway authorization independently reads the same flag from os.getenv at gateway/authz_mixin.py:426-429, so a secondary profile's scoped opt-in is still denied after intake.
  • Secondary startup validation has the same bypass: _start_one_profile_adapters invokes _own_policy_open_startup_violation inside the profile scope (gateway/run.py:8657-8660), but that validator reads the platform opt-in from os.getenv (gateway/run.py:1845-1851).
  • tools/send_message_tool.py:1863-1867 retains raw QQ credential fallbacks for the direct-send path.

Suggested changes

  • Apply the same scoped resolution to those gateway authorization, startup-validation, and direct-send paths; add end-to-end scope tests for QQ_ALLOW_ALL_USERS and all QQ_STT_* values.

Automated hermes-sweeper review.

@@ -3145,7 +3170,7 @@ def _strip_at_mention(content: str) -> str:
def _open_dm_opted_in(self) -> bool:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scopes the adapter intake gate, but GatewayAuthorizationMixin._is_user_authorized() separately checks QQ_ALLOW_ALL_USERS via raw os.getenv at gateway/authz_mixin.py:426-429; a secondary profile's scoped allow-all value will still be denied after intake. Please scope that gateway check (and the startup validator at gateway/run.py:1845-1851) in the same change.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 15, 2026
@Da7-Tech

Copy link
Copy Markdown
Contributor Author

Addressed the three flagged paths in 7875c0a20:

  • gateway/authz_mixin.py: per-platform allow-all, allowlist, and allow-bots reads now resolve through the scope-aware gateway.config._getenv (deployment-global GATEWAY_* reads intentionally stay raw). This makes the fix effective for every own-policy platform; unscoped behavior is identical to os.getenv.
  • gateway/run.py::_own_policy_open_startup_violation: dm/group policy + allow-all opt-in resolve via _getenv — the secondary-profile caller already runs inside _profile_runtime_scope.
  • tools/send_message_tool.py::_send_qqbot: QQ_APP_ID / QQ_CLIENT_SECRET fallbacks honor the active profile scope.

End-to-end tests added in tests/gateway/test_qqbot_scope_paths.py (authz / startup / direct-send: scope wins, no environ inheritance for non-opted profiles, single-profile fallback unchanged) plus QQ_STT_BASE_URL / QQ_STT_MODEL assertions in the STT suite. The five scoped-behavior tests fail on the previous head and pass on this one. Full tests/gateway run shows no regressions attributable to this change (the handful of failures reproduce identically on the unmodified base).

@Da7-Tech

Copy link
Copy Markdown
Contributor Author

Follow-up notes after an adversarial self-review of this change:

  • Added TestAuthzAllowlistScope (51704e913) so the scoped QQ_ALLOWED_USERS read is pinned too, not just the allow-all flag — the case fails if that read reverts to os.getenv.
  • Known residual (not introduced here, flagged for transparency): two startup paths — _schedule_resume_pending_sessions and the startup-restore replay in _drain_startup_restore_queue — run _is_user_authorized for a secondary-profile source outside any _profile_runtime_scope. There, _getenv falls back to os.environ (the primary profile's values), so a secondary profile's scoped allow-all/allowlist is not consulted on auto-resume/restore. This is byte-identical to the prior os.getenv behavior — this PR does not worsen it — but it's the same class and not fully closed on those two paths. Happy to wrap them per-session in the profile scope in this PR or a follow-up, whichever you prefer. The live inbound and busy/FIFO re-dispatch paths do inherit the scope correctly.

@teknium1 teknium1 added the area/profiles Multi-profile isolation, HERMES_HOME scoping label Jul 19, 2026
@egilewski

Copy link
Copy Markdown
Contributor

fully addressed

The three paths identified in the earlier review are addressed on the current head, and the 17 focused QQ credential/authorization tests passed when the patch was replayed onto current main.

I still think extending this PR to enter the appropriate profile scope in _schedule_resume_pending_sessions() and _drain_startup_restore_queue() would be the cleaner outcome: those paths cross the same profile-isolation boundary as the authorization changes here. However, that residual predates this PR and the current changes do not worsen it, so I would not treat it as a blocker for this PR. If it is not included here, it should be tracked in an independent issue or PR.

The submitted branch currently conflicts with main, so it still needs a rebase/conflict resolution and a focused test rerun before merge.

Signed: GPT-5.6-sol-xhigh in Codex

teknium1 pushed a commit that referenced this pull request Jul 28, 2026
…cope

`resolve_openai_audio_api_key()` reads the key that authenticates the audio
client straight from the process environment:

    return (
        os.getenv("VOICE_TOOLS_OPENAI_KEY", "")
        or os.getenv("OPENAI_API_KEY", "")
    ).strip()

That value is not advisory. It flows through
`_resolve_openai_audio_client_config()` into `OpenAIClient(api_key=...)` for
TTS, and through `transcription_tools` for voice-note STT — both on the
per-turn tool path, inside the profile secret scope the gateway installs.

`agent/vertex_adapter` states the contract this breaks:

    in a multiplex gateway serving several profiles from one process,
    os.environ reflects whichever profile's .env happened to be loaded at
    boot, not the profile the current turn belongs to. Reading it directly
    here would let one profile mint tokens from — and get billed against —
    a different profile's service-account file.

Reproduced with the real resolver, multiplexing on and profile A's scope
installed:

    scope-aware get_secret  -> sk-PROFILE-A-key
    voice/STT resolver      -> sk-PROFILE-B-key

So profile A's spoken reply and its users' voice notes are sent to OpenAI on
profile B's account, and billed there.

Route both reads through `agent.secret_scope.get_secret`, the same fix already
merged for the WeChat send path (#59662) and pending for QQ (#60420) — neither
covers the audio credential family. Under multiplexing the scope stays
authoritative, so a scope miss now yields no key instead of borrowing another
profile's; with multiplexing off `get_secret` falls through to `os.environ`
exactly as before, so single-profile deployments are untouched. The
VOICE_TOOLS_OPENAI_KEY > OPENAI_API_KEY precedence is unchanged.

Deliberately narrow: `fal_key_is_configured()` and
`has_direct_modal_credentials()` in this file are presence checks, not
authentication, and the former is already being reworked in open PR #20929.

tests/tools/test_tool_backend_helpers.py: the scope wins over another
profile's `os.environ`; a scope miss does not borrow another profile's key;
voice-key precedence holds inside a scope; and a control proves the
single-profile path still reads `os.environ`. The three isolation tests fail
on main; the control passes there. 320 passed across the helper, secret-scope,
and consumer suites (the fluctuating voice_mode/voice_cli failures are
pre-existing PulseAudio/ordering artifacts — the differing test passes 3/3 in
isolation on both main and this branch).
The QQ adapter read QQ_APP_ID, QQ_CLIENT_SECRET, the QQ_STT_* backend
config and the QQ_ALLOW_ALL_USERS policy flag through raw os.getenv,
bypassing the active profile secret scope. In multiplex mode a secondary
profile whose secret lives in its own .env (installed as an isolated
scope, not into os.environ) would silently fall back to the
default/primary profile's value — the same cross-profile collision fixed
for the WeChat/weixin adapter in NousResearch#59662.

Route these reads through a scope-aware resolver that reads the profile
scope when one is installed (secondary profiles and per-turn inbound) and
falls back to os.environ otherwise. The fallback is deliberate: the
primary/active profile is constructed without a scope and owns
os.environ, so a bare get_secret would raise UnscopedSecretError and
break its startup. Mirrors gateway.config._getenv.

Adds regression tests including active-profile-no-scope construction (the
fail-closed case), plus scope-wins-over-environ, two-profile isolation,
single-profile fallback, explicit-config precedence and STT key scoping.
…eads

Review follow-up: the adapter-level resolver alone left three paths
reading per-profile QQ_* values from raw os.getenv, so a secondary
multiplex profile's scoped opt-in or credentials were ignored (or the
primary's environ values leaked in):

- gateway/authz_mixin.py: route the per-platform allow-all flag and the
  per-platform/group allowlist + allow-bots reads through the
  scope-aware gateway.config._getenv. Deployment-global GATEWAY_* reads
  intentionally stay on os.getenv. This makes the same fix effective
  for every own-policy platform, not just QQ; unscoped behavior is
  byte-identical to os.getenv.
- gateway/run.py (_own_policy_open_startup_violation): resolve the
  per-platform dm/group policy and allow-all opt-in via _getenv; the
  secondary-profile caller already runs inside _profile_runtime_scope.
- tools/send_message_tool.py (_send_qqbot): the QQ_APP_ID /
  QQ_CLIENT_SECRET fallbacks now honor the active profile scope.

Tests: tests/gateway/test_qqbot_scope_paths.py covers all three paths
end-to-end (scope wins, no environ inheritance for non-opted profiles,
single-profile environ fallback unchanged); the STT suite now asserts
QQ_STT_BASE_URL and QQ_STT_MODEL scoping alongside the API key. All
five scoped-behavior tests fail on the previous commit and pass here.
Follow-up to the review-hardening commit: the existing cases exercised
QQ_ALLOW_ALL_USERS but not the QQ_ALLOWED_USERS read at authz_mixin.py
line 459, so a revert of that line to raw os.getenv would still pass.
Add a scoped-allowlist DM case (scope admits the sender, environ does
not) plus its isolation counterpart (a secondary scope listing a
different user must not inherit the primary's environ allowlist). Both
fail if line 459 reverts to os.getenv.
@Da7-Tech
Da7-Tech force-pushed the fix/qqbot-get-secret-profile-isolation branch from 51704e9 to eec5531 Compare August 2, 2026 08:51
teknium1 added a commit that referenced this pull request Aug 2, 2026
…ixin gate PR

The cherry-picked #60420 hunks that converted gateway/authz_mixin.py are
dropped here: main's _auth_env/_platform_gate_env supersede them, and the
remaining authz_mixin raw-read conversions (allow-all flag + allowlists at
L459/501/879-885) land in a separate PR. Until that PR flips the allow-all
read to scope-authoritative semantics, the cross-profile environ-opt-in
inheritance case is a known gap — pin it as strict xfail so the separate PR
flips it green.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…cope

`resolve_openai_audio_api_key()` reads the key that authenticates the audio
client straight from the process environment:

    return (
        os.getenv("VOICE_TOOLS_OPENAI_KEY", "")
        or os.getenv("OPENAI_API_KEY", "")
    ).strip()

That value is not advisory. It flows through
`_resolve_openai_audio_client_config()` into `OpenAIClient(api_key=...)` for
TTS, and through `transcription_tools` for voice-note STT — both on the
per-turn tool path, inside the profile secret scope the gateway installs.

`agent/vertex_adapter` states the contract this breaks:

    in a multiplex gateway serving several profiles from one process,
    os.environ reflects whichever profile's .env happened to be loaded at
    boot, not the profile the current turn belongs to. Reading it directly
    here would let one profile mint tokens from — and get billed against —
    a different profile's service-account file.

Reproduced with the real resolver, multiplexing on and profile A's scope
installed:

    scope-aware get_secret  -> sk-PROFILE-A-key
    voice/STT resolver      -> sk-PROFILE-B-key

So profile A's spoken reply and its users' voice notes are sent to OpenAI on
profile B's account, and billed there.

Route both reads through `agent.secret_scope.get_secret`, the same fix already
merged for the WeChat send path (NousResearch#59662) and pending for QQ (NousResearch#60420) — neither
covers the audio credential family. Under multiplexing the scope stays
authoritative, so a scope miss now yields no key instead of borrowing another
profile's; with multiplexing off `get_secret` falls through to `os.environ`
exactly as before, so single-profile deployments are untouched. The
VOICE_TOOLS_OPENAI_KEY > OPENAI_API_KEY precedence is unchanged.

Deliberately narrow: `fal_key_is_configured()` and
`has_direct_modal_credentials()` in this file are presence checks, not
authentication, and the former is already being reworked in open PR NousResearch#20929.

tests/tools/test_tool_backend_helpers.py: the scope wins over another
profile's `os.environ`; a scope miss does not borrow another profile's key;
voice-key precedence holds inside a scope; and a control proves the
single-profile path still reads `os.environ`. The three isolation tests fail
on main; the control passes there. 320 passed across the helper, secret-scope,
and consumer suites (the fluctuating voice_mode/voice_cli failures are
pre-existing PulseAudio/ordering artifacts — the differing test passes 3/3 in
isolation on both main and this branch).
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…ixin gate PR

The cherry-picked NousResearch#60420 hunks that converted gateway/authz_mixin.py are
dropped here: main's _auth_env/_platform_gate_env supersede them, and the
remaining authz_mixin raw-read conversions (allow-all flag + allowlists at
L459/501/879-885) land in a separate PR. Until that PR flips the allow-all
read to scope-authoritative semantics, the cross-profile environ-opt-in
inheritance case is a known gap — pin it as strict xfail so the separate PR
flips it green.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…cope

`resolve_openai_audio_api_key()` reads the key that authenticates the audio
client straight from the process environment:

    return (
        os.getenv("VOICE_TOOLS_OPENAI_KEY", "")
        or os.getenv("OPENAI_API_KEY", "")
    ).strip()

That value is not advisory. It flows through
`_resolve_openai_audio_client_config()` into `OpenAIClient(api_key=...)` for
TTS, and through `transcription_tools` for voice-note STT — both on the
per-turn tool path, inside the profile secret scope the gateway installs.

`agent/vertex_adapter` states the contract this breaks:

    in a multiplex gateway serving several profiles from one process,
    os.environ reflects whichever profile's .env happened to be loaded at
    boot, not the profile the current turn belongs to. Reading it directly
    here would let one profile mint tokens from — and get billed against —
    a different profile's service-account file.

Reproduced with the real resolver, multiplexing on and profile A's scope
installed:

    scope-aware get_secret  -> sk-PROFILE-A-key
    voice/STT resolver      -> sk-PROFILE-B-key

So profile A's spoken reply and its users' voice notes are sent to OpenAI on
profile B's account, and billed there.

Route both reads through `agent.secret_scope.get_secret`, the same fix already
merged for the WeChat send path (NousResearch#59662) and pending for QQ (NousResearch#60420) — neither
covers the audio credential family. Under multiplexing the scope stays
authoritative, so a scope miss now yields no key instead of borrowing another
profile's; with multiplexing off `get_secret` falls through to `os.environ`
exactly as before, so single-profile deployments are untouched. The
VOICE_TOOLS_OPENAI_KEY > OPENAI_API_KEY precedence is unchanged.

Deliberately narrow: `fal_key_is_configured()` and
`has_direct_modal_credentials()` in this file are presence checks, not
authentication, and the former is already being reworked in open PR NousResearch#20929.

tests/tools/test_tool_backend_helpers.py: the scope wins over another
profile's `os.environ`; a scope miss does not borrow another profile's key;
voice-key precedence holds inside a scope; and a control proves the
single-profile path still reads `os.environ`. The three isolation tests fail
on main; the control passes there. 320 passed across the helper, secret-scope,
and consumer suites (the fluctuating voice_mode/voice_cli failures are
pre-existing PulseAudio/ordering artifacts — the differing test passes 3/3 in
isolation on both main and this branch).
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…ixin gate PR

The cherry-picked NousResearch#60420 hunks that converted gateway/authz_mixin.py are
dropped here: main's _auth_env/_platform_gate_env supersede them, and the
remaining authz_mixin raw-read conversions (allow-all flag + allowlists at
L459/501/879-885) land in a separate PR. Until that PR flips the allow-all
read to scope-authoritative semantics, the cross-profile environ-opt-in
inheritance case is a known gap — pin it as strict xfail so the separate PR
flips it green.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…cope

`resolve_openai_audio_api_key()` reads the key that authenticates the audio
client straight from the process environment:

    return (
        os.getenv("VOICE_TOOLS_OPENAI_KEY", "")
        or os.getenv("OPENAI_API_KEY", "")
    ).strip()

That value is not advisory. It flows through
`_resolve_openai_audio_client_config()` into `OpenAIClient(api_key=...)` for
TTS, and through `transcription_tools` for voice-note STT — both on the
per-turn tool path, inside the profile secret scope the gateway installs.

`agent/vertex_adapter` states the contract this breaks:

    in a multiplex gateway serving several profiles from one process,
    os.environ reflects whichever profile's .env happened to be loaded at
    boot, not the profile the current turn belongs to. Reading it directly
    here would let one profile mint tokens from — and get billed against —
    a different profile's service-account file.

Reproduced with the real resolver, multiplexing on and profile A's scope
installed:

    scope-aware get_secret  -> sk-PROFILE-A-key
    voice/STT resolver      -> sk-PROFILE-B-key

So profile A's spoken reply and its users' voice notes are sent to OpenAI on
profile B's account, and billed there.

Route both reads through `agent.secret_scope.get_secret`, the same fix already
merged for the WeChat send path (NousResearch#59662) and pending for QQ (NousResearch#60420) — neither
covers the audio credential family. Under multiplexing the scope stays
authoritative, so a scope miss now yields no key instead of borrowing another
profile's; with multiplexing off `get_secret` falls through to `os.environ`
exactly as before, so single-profile deployments are untouched. The
VOICE_TOOLS_OPENAI_KEY > OPENAI_API_KEY precedence is unchanged.

Deliberately narrow: `fal_key_is_configured()` and
`has_direct_modal_credentials()` in this file are presence checks, not
authentication, and the former is already being reworked in open PR NousResearch#20929.

tests/tools/test_tool_backend_helpers.py: the scope wins over another
profile's `os.environ`; a scope miss does not borrow another profile's key;
voice-key precedence holds inside a scope; and a control proves the
single-profile path still reads `os.environ`. The three isolation tests fail
on main; the control passes there. 320 passed across the helper, secret-scope,
and consumer suites (the fluctuating voice_mode/voice_cli failures are
pre-existing PulseAudio/ordering artifacts — the differing test passes 3/3 in
isolation on both main and this branch).
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…ixin gate PR

The cherry-picked NousResearch#60420 hunks that converted gateway/authz_mixin.py are
dropped here: main's _auth_env/_platform_gate_env supersede them, and the
remaining authz_mixin raw-read conversions (allow-all flag + allowlists at
L459/501/879-885) land in a separate PR. Until that PR flips the allow-all
read to scope-authoritative semantics, the cross-profile environ-opt-in
inheritance case is a known gap — pin it as strict xfail so the separate PR
flips it green.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/qqbot QQ Bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants