fix(auth): per-profile Anthropic OAuth file + complete port-binding platform set (#57563 salvage) (#59339)

* fix(auth): resolve Anthropic OAuth file per-profile + close port-binding platform gaps

Two focused pieces salvaged from PR #57563:

1. _HERMES_OAUTH_FILE was computed at module import time — frozen before
   HERMES_HOME/profile overrides, so multiplexed profile turns read and
   wrote the DEFAULT profile's .anthropic_oauth.json (OAuth path hijack).
   Replaced with a lazy _get_hermes_oauth_file(); all web_server.py call
   sites updated.

2. _PORT_BINDING_PLATFORM_VALUES was missing whatsapp_cloud and line —
   both bind aiohttp TCP listeners, so a secondary multiplex profile
   enabling them would collide with the primary's listener instead of
   failing fast at startup.

Original work by @austinlaw076. The rest of #57563 was redundant on
main (adapter routing sweep superseded by #56854's salvage; cron secret
scope landed in fdab380a1; nested-config fallback in from_dict).

* chore(release): map austinlaw076 author email for PR #57563 salvage

* test(hermes_cli): patch _get_hermes_oauth_file instead of removed _HERMES_OAUTH_FILE constant

---------

Co-authored-by: Austin <austin@openvm067.space>
Co-authored-by: Ben <ben@nousresearch.com>
fix/verification-admin-route-recovery
Teknium 2026-07-07 01:33:06 -07:00 committed by GitHub
parent 249c69b958
commit 76979a0869
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 23 additions and 16 deletions

View File

@ -1390,7 +1390,8 @@ _OAUTH_TOKEN_URL = _OAUTH_TOKEN_URLS[0]
_OAUTH_TOKEN_USER_AGENT = "axios/1.7.9"
_OAUTH_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"
_OAUTH_SCOPES = "org:create_api_key user:profile user:inference"
_HERMES_OAUTH_FILE = get_hermes_home() / ".anthropic_oauth.json"
def _get_hermes_oauth_file() -> Path:
return get_hermes_home() / ".anthropic_oauth.json"
def _generate_pkce() -> tuple:
@ -1538,9 +1539,10 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]:
def read_hermes_oauth_credentials() -> Optional[Dict[str, Any]]:
"""Read Hermes-managed OAuth credentials from ~/.hermes/.anthropic_oauth.json."""
if _HERMES_OAUTH_FILE.exists():
oauth_file = _get_hermes_oauth_file()
if oauth_file.exists():
try:
data = json.loads(_HERMES_OAUTH_FILE.read_text(encoding="utf-8"))
data = json.loads(oauth_file.read_text(encoding="utf-8"))
if data.get("accessToken"):
return data
except (json.JSONDecodeError, OSError, IOError) as e:

View File

@ -1363,6 +1363,8 @@ _PORT_BINDING_PLATFORM_VALUES = frozenset({
"wecom_callback",
"bluebubbles",
"sms",
"whatsapp_cloud",
"line",
})

View File

@ -6443,11 +6443,11 @@ def _anthropic_oauth_status() -> Dict[str, Any]:
try:
from agent.anthropic_adapter import (
read_hermes_oauth_credentials,
_HERMES_OAUTH_FILE,
_get_hermes_oauth_file,
)
except ImportError:
read_hermes_oauth_credentials = None # type: ignore
_HERMES_OAUTH_FILE = None # type: ignore
_get_hermes_oauth_file = None # type: ignore
hermes_creds = None
if read_hermes_oauth_credentials:
@ -6459,7 +6459,7 @@ def _anthropic_oauth_status() -> Dict[str, Any]:
return {
"logged_in": True,
"source": "hermes_pkce",
"source_label": f"Hermes PKCE ({_HERMES_OAUTH_FILE})",
"source_label": f"Hermes PKCE ({_get_hermes_oauth_file() if _get_hermes_oauth_file else None})",
"token_preview": _truncate_token(hermes_creds.get("accessToken")),
"expires_at": hermes_creds.get("expiresAt"),
"has_refresh_token": bool(hermes_creds.get("refreshToken")),
@ -6896,9 +6896,10 @@ async def disconnect_oauth_provider(
if provider_id == "anthropic":
cleared = False
try:
from agent.anthropic_adapter import _HERMES_OAUTH_FILE
if _HERMES_OAUTH_FILE.exists():
_HERMES_OAUTH_FILE.unlink()
from agent.anthropic_adapter import _get_hermes_oauth_file
oauth_file = _get_hermes_oauth_file()
if oauth_file.exists():
oauth_file.unlink()
cleared = True
except Exception:
pass
@ -7042,24 +7043,25 @@ def _save_anthropic_oauth_creds(access_token: str, refresh_token: str, expires_a
Mirrors what auth_commands.add_command does so the dashboard flow leaves
the system in the same state as ``hermes auth add anthropic``.
"""
from agent.anthropic_adapter import _HERMES_OAUTH_FILE
from agent.anthropic_adapter import _get_hermes_oauth_file
oauth_file = _get_hermes_oauth_file()
payload = {
"accessToken": access_token,
"refreshToken": refresh_token,
"expiresAt": expires_at_ms,
}
_HERMES_OAUTH_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp_path = _HERMES_OAUTH_FILE.with_name(
f"{_HERMES_OAUTH_FILE.name}.tmp.{os.getpid()}.{secrets.token_hex(8)}"
oauth_file.parent.mkdir(parents=True, exist_ok=True)
tmp_path = oauth_file.with_name(
f"{oauth_file.name}.tmp.{os.getpid()}.{secrets.token_hex(8)}"
)
try:
with tmp_path.open("w", encoding="utf-8") as handle:
handle.write(json.dumps(payload, indent=2))
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp_path, _HERMES_OAUTH_FILE)
os.replace(tmp_path, oauth_file)
try:
_HERMES_OAUTH_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
oauth_file.chmod(stat.S_IRUSR | stat.S_IWUSR)
except OSError:
pass
finally:

View File

@ -52,6 +52,7 @@ AUTHOR_MAP = {
"AndreasHiltner@users.noreply.github.com": "AndreasHiltner", # PR #56854 salvage (gateway: route multiplex profile responses through the profile's own adapter — 53-site _adapter_for_source sweep)
"allenliang2022@users.noreply.github.com": "allenliang2022", # PR #56932 test coverage folded into #56909 salvage (408 → retryable timeout)
"m888.braun@hotmail.com": "ManniBr", # PR #57417 partial salvage (gateway: fail-closed adapter resolution for unregistered secondary profiles)
"austin@openvm067.space": "austinlaw076", # PR #57563 partial salvage (auth: lazy per-profile Anthropic OAuth file; gateway: whatsapp_cloud/line added to port-binding platform set)
"blueirobin02@gmail.com": "irresi", # PR #59048 salvage (gateway: scope reset banners' session info to the serving profile; #59003)
"jashlee+microsoft@microsoft.com": "s905060", # PR #57943 salvage (photon: auto-reinstall stale sidecar node_modules when lockfile is newer than npm's install marker; #59169)
"lohinth25@proton.me": "l0h1nth", # PR #32210 salvage (mattermost: accept leading-space slash commands from mobile clients; #25184)

View File

@ -19,7 +19,7 @@ class _DummyPool:
@pytest.fixture
def oauth_file(monkeypatch, tmp_path):
target = tmp_path / '.anthropic_oauth.json'
monkeypatch.setattr('agent.anthropic_adapter._HERMES_OAUTH_FILE', target)
monkeypatch.setattr('agent.anthropic_adapter._get_hermes_oauth_file', lambda: target)
monkeypatch.setattr('agent.credential_pool.load_pool', lambda _provider: _DummyPool())
return target