revert: keep Google Chat OAuth secret + active_provider profile-scoped (#39398)
* Revert "fix(gateway): anchor Google Chat OAuth client secret to default Hermes root" This reverts commitfff0561441. * Revert "fix(cli): honor global-root active_provider fallback for named profiles" This reverts commit3858cf4307. * docs(google_chat): describe OAuth client secret as profile-scoped, not host-wide The setup docs, oauth docstring, and the adapter's 'no credentials' error message all described the Google Chat OAuth client secret as host-wide shared infrastructure. That contradicts profile isolation: profiles are separate auth boundaries, so two profiles can point at different Google OAuth apps / accounts. Reword all three to say the secret is profile-scoped and each profile registers its own.
This commit is contained in:
@ -16,7 +16,6 @@ import json
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@ -1651,48 +1650,6 @@ class TestUserOAuthHelper:
|
||||
},
|
||||
)
|
||||
|
||||
def test_client_secret_is_shared_across_profiles(self, tmp_path, monkeypatch):
|
||||
"""The OAuth client secret is host-wide infra: a secret seeded at the
|
||||
default root by the documented one-time `--client-secret` host step
|
||||
must be visible to a gateway running under a named profile.
|
||||
|
||||
Regression: `_client_secret_path()` used to scope to the active
|
||||
HERMES_HOME, so a profile gateway reported 'No client credentials
|
||||
stored on the host' even after the host setup had been run.
|
||||
"""
|
||||
root = tmp_path / ".hermes"
|
||||
profile_home = root / "profiles" / "bot1"
|
||||
profile_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
|
||||
# Seed the secret at the default root, as the host setup does.
|
||||
secret = root / "google_chat_user_client_secret.json"
|
||||
secret.write_text("{}", encoding="utf-8")
|
||||
|
||||
# Resolve from inside a named profile.
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile_home))
|
||||
from plugins.platforms.google_chat.oauth import _client_secret_path
|
||||
assert _client_secret_path() == secret
|
||||
assert _client_secret_path().exists()
|
||||
|
||||
def test_profile_local_client_secret_takes_precedence(self, tmp_path, monkeypatch):
|
||||
"""A profile-local secret (separate OAuth app per bot, or a legacy
|
||||
profile-scoped seed) overrides the host-wide default when present."""
|
||||
root = tmp_path / ".hermes"
|
||||
profile_home = root / "profiles" / "bot1"
|
||||
profile_home.mkdir(parents=True)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(profile_home))
|
||||
|
||||
(root / "google_chat_user_client_secret.json").write_text(
|
||||
"{}", encoding="utf-8"
|
||||
)
|
||||
profile_secret = profile_home / "google_chat_user_client_secret.json"
|
||||
profile_secret.write_text("{}", encoding="utf-8")
|
||||
|
||||
from plugins.platforms.google_chat.oauth import _client_secret_path
|
||||
assert _client_secret_path() == profile_secret
|
||||
|
||||
def test_store_client_secret_writes_private_json(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
src = tmp_path / "client_secret.json"
|
||||
|
||||
@ -17,18 +17,12 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_auth_store(
|
||||
pool: dict | None = None,
|
||||
providers: dict | None = None,
|
||||
active_provider: str | None = None,
|
||||
) -> dict:
|
||||
def _make_auth_store(pool: dict | None = None, providers: dict | None = None) -> dict:
|
||||
store: dict = {"version": 1}
|
||||
if pool is not None:
|
||||
store["credential_pool"] = pool
|
||||
if providers is not None:
|
||||
store["providers"] = providers
|
||||
if active_provider is not None:
|
||||
store["active_provider"] = active_provider
|
||||
return store
|
||||
|
||||
|
||||
@ -456,101 +450,3 @@ def test_write_credential_pool_targets_profile_not_global(profile_env):
|
||||
|
||||
# Subsequent read returns profile (shadows global).
|
||||
assert [e["id"] for e in read_credential_pool("openrouter")] == ["prof-new"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_active_provider — global active_provider fallback (issue #18594 follow-up)
|
||||
#
|
||||
# The per-provider state/pool fallbacks let a profile *read* a provider that
|
||||
# was only authenticated at the global root, but ``resolve_provider()`` picks
|
||||
# the ``auto`` provider from ``active_provider`` — which only ever read the
|
||||
# profile store. A named profile running ``model.provider: auto`` could see
|
||||
# the global Nous login (``get_provider_auth_state('nous')`` succeeds) yet
|
||||
# still fail to select it. These pin the active_provider shadowing so the
|
||||
# selection mirrors the state/pool fallbacks: profile wins when present, fall
|
||||
# back to global when the profile never chose its own provider.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_active_provider_falls_back_to_global(profile_env):
|
||||
"""An empty profile inherits the global-root active_provider selection."""
|
||||
from hermes_cli.auth import get_active_provider
|
||||
|
||||
_write(profile_env["global"] / "auth.json", _make_auth_store(
|
||||
providers={"nous": {"access_token": "nous-global"}},
|
||||
active_provider="nous",
|
||||
))
|
||||
_write(profile_env["profile"] / "auth.json", _make_auth_store(providers={}))
|
||||
|
||||
assert get_active_provider() == "nous"
|
||||
|
||||
|
||||
def test_active_provider_profile_wins_over_global(profile_env):
|
||||
"""A profile that selected its own provider shadows the global selection."""
|
||||
from hermes_cli.auth import get_active_provider
|
||||
|
||||
_write(profile_env["global"] / "auth.json", _make_auth_store(
|
||||
providers={"nous": {"access_token": "nous-global"}},
|
||||
active_provider="nous",
|
||||
))
|
||||
_write(profile_env["profile"] / "auth.json", _make_auth_store(
|
||||
providers={"anthropic": {"access_token": "ant-profile"}},
|
||||
active_provider="anthropic",
|
||||
))
|
||||
|
||||
assert get_active_provider() == "anthropic"
|
||||
|
||||
|
||||
def test_active_provider_none_when_neither_has_it(profile_env):
|
||||
"""No selection anywhere stays None — the fallback must not invent one."""
|
||||
from hermes_cli.auth import get_active_provider
|
||||
|
||||
_write(profile_env["global"] / "auth.json", _make_auth_store(providers={}))
|
||||
_write(profile_env["profile"] / "auth.json", _make_auth_store(providers={}))
|
||||
|
||||
assert get_active_provider() is None
|
||||
|
||||
|
||||
def test_active_provider_classic_mode_reads_profile(tmp_path, monkeypatch):
|
||||
"""In classic mode there is no global to fall back to; behavior is unchanged."""
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", lambda: fake_home)
|
||||
hermes_home = tmp_path / "classic"
|
||||
hermes_home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
_write(hermes_home / "auth.json", _make_auth_store(
|
||||
providers={"nous": {"access_token": "classic-token"}},
|
||||
active_provider="nous",
|
||||
))
|
||||
|
||||
from hermes_cli.auth import get_active_provider
|
||||
|
||||
assert get_active_provider() == "nous"
|
||||
|
||||
|
||||
def test_resolve_provider_uses_global_active_provider(profile_env, monkeypatch):
|
||||
"""resolve_provider('auto') honors the global-root active_provider.
|
||||
|
||||
This is the user-visible contract: a named profile with no provider entry
|
||||
of its own, started with ``model.provider: auto`` while a valid login
|
||||
exists at the global root, resolves that provider instead of raising
|
||||
``No inference provider configured``. ``get_auth_status`` is stubbed so the
|
||||
login check stays offline (no Nous token refresh / network).
|
||||
"""
|
||||
import hermes_cli.auth as auth
|
||||
|
||||
_write(profile_env["global"] / "auth.json", _make_auth_store(
|
||||
providers={"nous": {"access_token": "nous-global"}},
|
||||
active_provider="nous",
|
||||
))
|
||||
_write(profile_env["profile"] / "auth.json", _make_auth_store(providers={}))
|
||||
|
||||
monkeypatch.setattr(
|
||||
auth,
|
||||
"get_auth_status",
|
||||
lambda provider=None: {"logged_in": True, "provider": provider},
|
||||
)
|
||||
|
||||
assert auth.resolve_provider("auto") == "nous"
|
||||
|
||||
Reference in New Issue
Block a user